Exis.PdfEditor — The .NET PDF Toolkit Exis.PdfEditor — The Python PDF Toolkit

Find & replace text in PDFs, merge, split, build, fill forms, redact, sign, optimize, and convert to PDF/A — all from your C# code. Zero dependencies. Lossless editing. Find & replace text in PDFs, merge, split, build, fill forms, redact, sign, optimize, and convert to PDF/A — all from your Python code. Native AOT-compiled binary, no .NET runtime required.

.NET CLI dotnet add package Exis.PdfEditor

PM Console Install-Package Exis.PdfEditor
pip pip install exis-pdfeditor

poetry poetry add exis-pdfeditor

See it in Action

Watch a 5-minute walkthrough of the library — from dotnet add package to batch find & replace, regex patterns, and styled replacements. Console, WPF, and WinForms demo apps included.

Watch a walkthrough of the Python library — from pip install exis-pdfeditor to batch find & replace, regex patterns, and styled replacements. Browse the Python sample app on GitHub for working examples.

Why Exis.PdfEditor?

Most PDF libraries destroy your document

Most libraries convert your PDF to an intermediate format (HTML, image, or DOM), apply changes, then rebuild the entire file from scratch. This destructive render-rebuild cycle inevitably breaks the original document.

What gets broken:

  • Interactive form fields are flattened or lost
  • Digital signatures are invalidated
  • Character spacing and kerning are altered
  • Font substitution changes the appearance
  • Bookmarks, links, and annotations are dropped

Exis.PdfEditor edits content streams directly

Exis.PdfEditor parses the actual PDF content streams and performs surgical replacements at the operator level. No intermediate format. No rebuild. The output is the same file with only the targeted changes applied.

What gets preserved:

  • All form fields remain fully interactive
  • Digital signatures stay valid on unmodified pages
  • Original character spacing is maintained exactly
  • Embedded fonts are reused, not substituted
  • Bookmarks, annotations, and metadata are untouched

Other Libraries

PDF
Convert to intermediate format
Modify intermediate
Rebuild entire PDF
Damaged PDF (forms, sigs, spacing lost)

Exis.PdfEditor

PDF
Parse content streams
Replace at operator level
Incremental save
Identical PDF (only text changed)

Code Samples

Find & Replace Text (3 Lines of Code)

using Exis.PdfEditor;
using Exis.PdfEditor.Licensing;

ExisLicense.Initialize();  // Complimentary 14-day trial - no key needed

var result = PdfFindReplace.Execute(
    "contract.pdf",
    "contract-updated.pdf",
    "Acme Corporation",
    "Globex Industries");

Console.WriteLine($"Replaced {result.TotalReplacements} occurrences " +
                  $"across {result.PagesModified} pages.");

Find & Replace Text (3 Lines of Code)

import exis_pdfeditor

exis_pdfeditor.initialize()  # Complimentary 14-day trial - no key needed

result = exis_pdfeditor.find_replace(
    "contract.pdf",
    "contract-updated.pdf",
    "Acme Corporation",
    "Globex Industries",
)

print(f"Replaced {result.totalReplacements} occurrences "
      f"across {result.pagesModified} pages.")

Multiple Find & Replace Pairs

var pairs = new[]
{
    new FindReplacePair("2025", "2026"),
    new FindReplacePair("Draft", "Final"),
    new FindReplacePair("CONFIDENTIAL", "PUBLIC"),
};

var result = PdfFindReplace.Execute(
    "report.pdf",
    "report-final.pdf",
    pairs);

Multiple Find & Replace Pairs

result = exis_pdfeditor.find_replace(
    "report.pdf",
    "report-final.pdf",
    pairs=[
        {"search": "2025",         "replace": "2026"},
        {"search": "Draft",        "replace": "Final"},
        {"search": "CONFIDENTIAL", "replace": "PUBLIC"},
    ],
)

Regex Pattern Matching

var options = new PdfFindReplaceOptions { UseRegex = true };

// Replace all US phone numbers with a placeholder
var result = PdfFindReplace.Execute(
    "document.pdf",
    "redacted.pdf",
    @"\(\d{3}\)\s?\d{3}-\d{4}",
    "[PHONE REDACTED]",
    options);

Regex Pattern Matching

# Replace all US phone numbers with a placeholder
result = exis_pdfeditor.find_replace(
    "document.pdf",
    "redacted.pdf",
    pairs=[
        {
            "search": r"\(\d{3}\)\s?\d{3}-\d{4}",
            "replace": "[PHONE REDACTED]",
            "isRegex": True,
        },
    ],
)

Licensed Usage

// Complimentary 14-day trial — no key needed, full features unlocked
ExisLicense.Initialize();

// Or, after purchase (pdfbatcheditor.com/developers — $499/developer/year)
ExisLicense.Initialize("XXXX-XXXX-XXXX-XXXX");

// Unlimited pages, no restrictions, no console messages
var result = PdfFindReplace.Execute("large-doc.pdf", "output.pdf", "old", "new");

Licensed Usage

# Complimentary 14-day trial — no key needed, full features unlocked
exis_pdfeditor.initialize()

# Or, after purchase (pdfbatcheditor.com/developers — $499/developer/year)
exis_pdfeditor.initialize("XXXX-XXXX-XXXX-XXXX")

# Or set EXIS_PDF_LICENSE_KEY environment variable

# Unlimited pages, no restrictions, no console messages
result = exis_pdfeditor.find_replace("large-doc.pdf", "output.pdf", "old", "new")

Text Fitting Options

var options = new PdfFindReplaceOptions
{
    CaseSensitive = true,
    WholeWordOnly = false,
    UseRegex = false,
    UseIncrementalUpdate = true,
    TextFitting = TextFittingMode.Adaptive,  // Best quality text fitting
    MinHorizontalScale = 70,                 // Minimum Tz percentage (50-100)
    MaxFontSizeReduction = 1.5               // Max font size reduction in points
};

var result = PdfFindReplace.Execute(
    "contract.pdf", "updated.pdf",
    "Short Name", "A Much Longer Replacement Name That Needs Fitting",
    options);

Text Fitting Options

result = exis_pdfeditor.find_replace(
    "contract.pdf",
    "updated.pdf",
    "Short Name",
    "A Much Longer Replacement Name That Needs Fitting",
    case_sensitive=True,
    whole_word=False,
    use_regex=False,
    use_incremental_update=True,
    text_fitting="adaptive",        # none | preserve_width | fit_to_page | adaptive
    min_horizontal_scale=70,        # Minimum Tz percentage (50-100)
    max_font_size_reduction=1.5,    # Max font size reduction in points
)

Text Formatting & Decorations

// Apply formatting to replacement text
var result = PdfFindReplace.Execute(
    "input.pdf", "output.pdf",
    "old text", "new text",
    new PdfFindReplaceOptions
    {
        ReplacementTextColor = PdfColor.Red,         // Font color of replaced text
        ReplacementHighlightColor = PdfColor.Yellow,  // Background highlight behind text
        ReplacementBold = true,                       // Faux bold via fill+stroke rendering
        ReplacementUnderline = true,                  // Draw underline below text
        ReplacementStrikethrough = true               // Draw line through text
    });

Text Formatting & Decorations

# Apply formatting to replacement text
result = exis_pdfeditor.find_replace(
    "input.pdf",
    "output.pdf",
    "old text",
    "new text",
    replacement_text_color={"r": 1, "g": 0, "b": 0},        # Red font
    replacement_highlight_color={"r": 1, "g": 1, "b": 0},   # Yellow highlight
    replacement_bold=True,                                   # Faux bold
    replacement_underline=True,
    replacement_strikethrough=True,
)

Merge PDFs

// Merge multiple PDFs into one, preserving page dimensions and resources
byte[] merged = PdfMerger.Merge(new[] { "cover.pdf", "report.pdf", "appendix.pdf" });
File.WriteAllBytes("combined.pdf", merged);

// Or write directly to a file
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
});

Merge PDFs

# Merge multiple PDFs into one, preserving page dimensions and resources
exis_pdfeditor.merge(
    ["cover.pdf", "report.pdf", "appendix.pdf"],
    "combined.pdf",
)

Split PDFs

// 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 with naming pattern
PdfSplitter.SplitToFiles("input.pdf", "page_{0}.pdf");

Split PDFs

# Split into individual page files
result = exis_pdfeditor.split("input.pdf", "output_folder/")
print(f"Split into {result.pageCount} files")

# Extract specific pages (1-based) into a new PDF
exis_pdfeditor.extract_pages("input.pdf", "subset.pdf", pages=[1, 3, 5])

Build PDFs 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(jpegBytes, x: 300, y: 400, width: 200, height: 150))
    .AddPage(page => page
        .Size(PdfPageSize.Letter)
        .AddText("Page 2", x: 72, y: 700, fontSize: 14))
    .Build();

File.WriteAllBytes("output.pdf", pdf);

Stamp / Overlay another PDF

# Overlay a letterhead on top of every page
result = exis_pdfeditor.stamp(
    "document.pdf",
    "stamped.pdf",
    "letterhead.pdf",
    mode="overlay",
)
print(f"Stamped {result.pagesStamped} pages")

# Or use as a background underlay
exis_pdfeditor.stamp(
    "document.pdf",
    "stamped.pdf",
    "background.pdf",
    mode="underlay",
    opacity=0.5,
    page_range=[1],
)

Extract Text

// Extract all text from a PDF
PdfTextResult text = PdfTextExtractor.ExtractText("input.pdf");
Console.WriteLine(text.FullText);

// Extract from specific pages only
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})");

Extract Text

# Extract all text from a PDF
result = exis_pdfeditor.extract_text("input.pdf")
print(result.fullText)

# Extract from specific pages only
partial = exis_pdfeditor.extract_text("input.pdf", pages=[1, 3])

# Structured extraction with position and font data
structured = exis_pdfeditor.extract_text_structured("input.pdf")
for block in structured.pages[0].textBlocks:
    print(f"[{block.x:.0f},{block.y:.0f}] {block.text} "
          f"(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}");

Inspect Document (No License Required)

info = exis_pdfeditor.inspect("input.pdf")

print(f"Pages: {info.pageCount}")
print(f"Title: {info.title}")
print(f"Fonts: {', '.join(info.fontsUsed)}")
print(f"Encrypted: {info.isEncrypted}")
print(f"Form fields: {info.formFieldCount}")

Image Replacement

// Find all images in a PDF
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)}");

// 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 } });

Image Replacement

# Find all images in a PDF
result = exis_pdfeditor.find_images("input.pdf")
for img in result.images:
    print(f"Image {img.index}: {img.pixelWidth}x{img.pixelHeight} "
          f"{img.colorSpace} {img.format} on page(s) {img.pageNumbers}")

# Replace all images with a new one
result = exis_pdfeditor.replace_image("input.pdf", "output.pdf", "new-logo.jpg")
print(f"Replaced {result.imagesReplaced} of {result.imagesFound} images")

# Replace specific images by index, scoped to a page
exis_pdfeditor.replace_image(
    "input.pdf", "output.pdf", "new-logo.jpg",
    image_indices=[0, 2],
    page_range=[1],
    scale_mode="scale_to_fit",
)

Auto-Layout Document Builder

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)
        .AlternatingRowBackground(0.95, 0.95, 1.0)
        .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();

Page Editing (rotate, crop, reorder, delete)

# Rotate all pages 90 degrees clockwise
exis_pdfeditor.rotate("in.pdf", "rotated.pdf", angle=90)

# Rotate only specific pages
exis_pdfeditor.rotate("in.pdf", "rotated.pdf", angle=180, pages=[2, 4])

# Crop pages to a rectangle
exis_pdfeditor.crop(
    "in.pdf", "cropped.pdf",
    rect={"x": 50, "y": 50, "width": 500, "height": 700},
)

# Reorder pages
exis_pdfeditor.reorder("in.pdf", "reordered.pdf", order=[3, 1, 2])

# Delete pages
exis_pdfeditor.delete_pages("in.pdf", "trimmed.pdf", pages=[2, 4])

Form Filling

// Read form fields
List<PdfFormField> fields = PdfFormFiller.GetFields("form.pdf");
foreach (var field in fields)
    Console.WriteLine($"{field.Name} ({field.FieldType}) = {field.CurrentValue}");

// Fill 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, remove interactive fields)
PdfFormFiller.Flatten("filled.pdf", "flattened.pdf");

Form Filling

# Read form fields
fields = exis_pdfeditor.list_fields("form.pdf")
for field in fields:
    print(f"{field.name} ({field.type}) = {field.value}")

# Fill fields
result = exis_pdfeditor.fill_form(
    "form.pdf", "filled.pdf",
    fields={
        "FirstName": "John",
        "LastName": "Doe",
        "State": "CA",
        "AgreeToTerms": "Yes",  # checkbox
    },
)
print(f"Filled {result.fieldsFilled} fields")

# Flatten in one call - merge values into page content
exis_pdfeditor.fill_form(
    "form.pdf", "flattened.pdf",
    fields={"FirstName": "John"},
    flatten=True,
)

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 specific page
    new PdfRedaction { PageNumber = 3, Area = new PdfRect(100, 200, 300, 50) }
});
Console.WriteLine($"Applied {result.RedactionsApplied} redactions");

Redaction

result = exis_pdfeditor.redact(
    "input.pdf", "redacted.pdf",
    redactions=[
        # Text-based redaction
        {"text": "CONFIDENTIAL"},

        # Regex pattern (e.g., SSN)
        {"text": r"\d{3}-\d{2}-\d{4}", "isRegex": True},

        # Replace with alternative text
        {"text": "SECRET", "replaceWith": "[REDACTED]"},

        # Area-based redaction on a specific page
        {
            "area": {"x": 100, "y": 200, "width": 300, "height": 50},
            "pageNumber": 3,
        },
    ],
)
print(f"Applied {result.redactionsApplied} redactions")

Optimization

var result = PdfOptimizer.Optimize("input.pdf", "optimized.pdf", new PdfOptimizeOptions
{
    CompressStreams = true,
    RemoveDuplicateObjects = true,
    RemoveMetadata = false,
    DownsampleImages = true,
    MaxImageDpi = 150
});
Console.WriteLine($"Saved {result.BytesSaved} bytes ({result.ReductionPercent:F1}%)");
Console.WriteLine($"Images downsampled: {result.ImagesDownsampled}");

Optimization

result = exis_pdfeditor.optimize(
    "input.pdf", "optimized.pdf",
    downsample_images=True,
    max_image_dpi=150,
    remove_metadata=False,
)
print(f"Reduced {result.originalSize:,} -> {result.optimizedSize:,} bytes "
      f"({result.reductionPercent:.1f}% smaller)")
print(f"Images downsampled: {result.imagesDownsampled}")

Digital Signatures (.NET 8, 9, 10+)

using System.Security.Cryptography.X509Certificates;

// Sign a PDF
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"
});

// 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 multi-signed document
List<PdfSignatureInfo> all = PdfSigner.VerifyAll("multi-signed.pdf");
foreach (var sig in all)
    Console.WriteLine($"{sig.SignerName}: valid={sig.IsValid}");

Digital Signatures

# Sign with an invisible signature
exis_pdfeditor.sign(
    "input.pdf", "signed.pdf",
    cert_path="certificate.pfx",
    cert_password="password",
    reason="Approved",
    location="New York",
    signer_name="John Doe",
)

# Sign with a visible signature box on page 1
exis_pdfeditor.sign(
    "input.pdf", "signed.pdf",
    cert_path="certificate.pfx",
    cert_password="password",
    visible=True,
    page=1,
    rect={"x": 50, "y": 50, "width": 200, "height": 60},
)

# Verify a signed PDF
info = exis_pdfeditor.verify("signed.pdf")
print(f"Signed: {info.isSigned}")
print(f"Valid:  {info.isValid}")
print(f"Signer: {info.signerName}")
print(f"Reason: {info.reason}")

# Verify all signatures in a multi-signed document
all_sigs = exis_pdfeditor.verify("multi-signed.pdf", all_signatures=True)
for sig in all_sigs:
    print(f"{sig.signerName}: valid={sig.isValid}")

PDF/A Compliance

// Validate (no license required)
// Levels: PdfA1b, PdfA2b, PdfA2u, PdfA3b, PdfA3u
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);

PDF/A Compliance

# Validate (no license required)
# Levels: 1b, 2b, 2u, 3b, 3u
result = exis_pdfeditor.pdfa_validate("input.pdf", level="2b")
print(f"Compliant: {result.isCompliant}")
for v in result.violations:
    print(f"  [{v.code}] {v.message} (auto-fix: {v.canAutoFix})")

# Convert to PDF/A
exis_pdfeditor.pdfa_convert("input.pdf", "output-pdfa.pdf", level="2b")

Watermark

// Diagonal watermark across all pages (default style)
PdfWatermark.AddText("input.pdf", "output.pdf", "CONFIDENTIAL");

// Custom: large red "DRAFT" across the page, 50% opacity, pages 1-3 only
PdfWatermark.AddText("input.pdf", "output.pdf", "DRAFT", new PdfWatermarkOptions
{
    Position = WatermarkPosition.Across, // Top | Bottom | Center | Across
    FontSize = 72,
    TextColor = PdfColor.Red,
    Opacity = 0.5,
    PageRange = new[] { 1, 2, 3 }
});

// Footer-style watermark on every page
PdfWatermark.AddText("input.pdf", "output.pdf", "Internal Use Only",
    new PdfWatermarkOptions
    {
        Position = WatermarkPosition.Bottom,
        FontSize = 14,
        Opacity = 0.5
    });

Watermark

# Basic diagonal watermark (default style)
result = exis_pdfeditor.watermark("input.pdf", "output.pdf", "DRAFT")
print(f"Watermarked {result.pagesWatermarked} of {result.totalPages} pages")

# Large red CONFIDENTIAL across the page, 15% opacity
exis_pdfeditor.watermark(
    "input.pdf", "output.pdf",
    "CONFIDENTIAL",
    position="across",      # top | bottom | center | across
    font_size=72,
    text_color={"r": 1, "g": 0, "b": 0},
    opacity=0.15,
)

# Footer-style watermark on page 1 only
exis_pdfeditor.watermark(
    "input.pdf", "footer.pdf",
    "Internal Use Only - Page 1",
    position="bottom",
    font_size=14,
    opacity=0.5,
    page_range=[1],
)

Bates Numbering

// Defaults: starts at 1, 6 digits, bottom-right corner
BatesStampResult result = PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf");
Console.WriteLine($"Stamped {result.PagesStamped} pages: " +
                  $"{result.FirstNumber} -> {result.LastNumber}");

// Full legal production: prefix, confidentiality label, continuous counter
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
    Prefix = "SMITH",
    StartNumber = 1,
    Digits = 6,                             // Produces SMITH000001, SMITH000002, ...
    Position = BatesPosition.BottomRight,   // Any of the 6 corners
    FontSize = 10,
    TextColor = PdfColor.Black,
    MarginInches = 0.5f,
    ConfidentialityLabel = "CONFIDENTIAL",  // Stamped on opposite edge
    SkipFirstPage = true,                   // Skip cover pages
    CounterAdvancesOnSkippedPages = false   // Counter holds during skips
});

// Continuous numbering across multiple files in list order
int next = 1;
foreach (var path in files)
{
    var r = PdfBatesStamp.ApplyBatesStamp(path, $"out/{Path.GetFileName(path)}",
        new BatesStampOptions { Prefix = "ABC", StartNumber = next, Digits = 6 });
    next = r.LastNumber + 1;
}

Bates Numbering

# Note: Bates numbering is a .NET-only capability in the current Exis.PdfEditor
# release. The PDF Batch Editor desktop app exposes it for macOS/Windows users,
# and the .NET library ships the PdfBatesStamp class above. A Python binding is
# on the roadmap — contact support@exisone.com if you need early access.

# Common workaround today: use the existing watermark() function with a
# per-page loop to emulate simple Bates stamps.
import exis_pdfeditor

info = exis_pdfeditor.inspect("input.pdf")
current = "input.pdf"
counter = 1
for page in range(1, info.pageCount + 1):
    stamp = f"SMITH{counter:06d}"
    next_out = f"_tmp_{page}.pdf"
    exis_pdfeditor.watermark(
        current, next_out, stamp,
        position="bottom",
        font_size=10,
        opacity=1.0,
        page_range=[page],
    )
    current = next_out
    counter += 1
# Final result is the last _tmp_ file; rename/move as desired.

XMP & Info Metadata

// Read both XMP packet and /Info dictionary. License-free, works on encrypted PDFs.
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
}

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}");

    foreach (var kv in info.Custom)   // Non-standard /Info keys
        Console.WriteLine($"[custom] {kv.Key} = {kv.Value}");
}

// Replace the /Info dictionary (trailer ref must already exist)
var newInfo = 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,
};
newInfo.Custom["Company"]  = "Exis LLC";
newInfo.Custom["Revision"] = "v3.6.3";

PdfXmpMetadata.SetInfo("input.pdf", "output.pdf", newInfo);

// Replace the XMP packet (RDF/XML wrapped in <?xpacket ...?>)
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);

// Wipe metadata for privacy (both XMP + /Info in one incremental update)
PdfXmpMetadata.RemoveAll("input.pdf", "stripped.pdf");
// Or drop only one form:
PdfXmpMetadata.RemoveXmp("input.pdf",  "stripped-xmp.pdf");
PdfXmpMetadata.RemoveInfo("input.pdf", "stripped-info.pdf");

XMP & Info Metadata

# Read both XMP packet and /Info dictionary. License-free, works on encrypted PDFs.
meta = exis_pdfeditor.get_metadata("document.pdf")

if meta.hasXmp:
    print(f"XMP packet: {meta.xmpByteSize} bytes")
    print(meta.xmpXml)          # Full <?xpacket ... ?> payload as UTF-8 text

if meta.hasInfo:
    info = meta.info
    print(f"Title:    {info.title}")
    print(f"Author:   {info.author}")
    print(f"Subject:  {info.subject}")
    print(f"Keywords: {info.keywords}")
    print(f"Creator:  {info.creator}")
    print(f"Producer: {info.producer}")
    print(f"Created:  {info.creationDate}")        # ISO 8601 string or None
    print(f"Modified: {info.modificationDate}")

    for key, value in vars(info.custom).items():   # Non-standard /Info keys
        print(f"  [custom] {key} = {value}")

# Replace the /Info dictionary (trailer ref must already exist)
from datetime import datetime, timezone

exis_pdfeditor.set_info(
    "input.pdf", "output.pdf",
    info={
        "title":    "Quarterly Report",
        "author":   "Finance Team",
        "subject":  "Q1 2026 earnings summary",
        "keywords": "earnings, Q1, 2026",
        "creator":  "exis-pdfeditor",
        "producer": "Exis.PdfEditor 3.6",
        "creationDate":     datetime(2026, 4, 1, 9, 0, tzinfo=timezone.utc),
        "modificationDate": datetime.now(timezone.utc),
        "custom": {
            "Company":  "Exis LLC",
            "Revision": "v3.6.4",
        },
    },
)

# Replace the XMP packet (RDF/XML wrapped in <?xpacket ...?>)
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"?>"""

exis_pdfeditor.set_xmp("input.pdf", "output.pdf", xmp)

# Wipe metadata for privacy (both XMP + /Info in one incremental update)
exis_pdfeditor.remove_metadata("input.pdf", "stripped.pdf")
# Or drop only one form:
exis_pdfeditor.remove_xmp("input.pdf",  "stripped-xmp.pdf")
exis_pdfeditor.remove_info("input.pdf", "stripped-info.pdf")

Async API

// All I/O operations have async overloads with CancellationToken support
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);

// Pattern: ClassName.MethodNameAsync(...) on all classes

Encrypt & Decrypt

# Encrypt with user (open) and owner (permissions) passwords
exis_pdfeditor.encrypt(
    "document.pdf", "protected.pdf",
    user_password="openme",
    owner_password="secret",
    permissions=["Print", "CopyText"],
    # Available: Print, ModifyContents, CopyText, AddAnnotations,
    #            FillForms, PrintHighQuality, All
)

# Decrypt to remove protection
exis_pdfeditor.decrypt("protected.pdf", "unlocked.pdf", password="openme")

How Exis.PdfEditor Compares

Feature Exis.PdfEditor IronPDF Spire.PDF Aspose.PDF Syncfusion
Direct content stream editing Renders via HTML/Chromium Redaction-style replacement Fragment-based replacement Redaction-style replacement
Preserve form fields Partial Partial
Preserve digital signatures On unmodified pages
Preserve character spacing Partial
Zero native dependencies Pure .NET Requires Chromium
DLL size < 500 KB ~250 MB ~20 MB ~40 MB ~15 MB
Batch processing Single-pass multi-pair Manual loop Manual loop Manual loop Manual loop
.NET Framework 4.8 .NET 6+ only
Cross-platform
Regex support
Price (per dev/year) $499 $749 $999 $1,175 $995*
Company HQ 🇺🇸 USA 🇺🇸 USA 🇨🇳 China 🇦🇺 Australia 🇺🇸 USA

* Syncfusion pricing requires a platform license. Prices are approximate and based on publicly available information as of 2026. Contact vendors for current pricing.
"Direct content stream editing" means the library modifies PDF content stream operators in-place rather than converting to an intermediate format and rebuilding.

Everything You Need for PDF Processing

📄

Direct Content Stream Editing

Edit PDF text at the operator level. No intermediate format, no render-rebuild. Your document comes out identical except for the targeted changes.

🔗

Zero Native Dependencies

Pure .NET assembly under 500 KB. No Chromium, no native DLLs, no system-level installs. Works everywhere .NET runs.

🔒

Lossless Editing

Forms, signatures, spacing, fonts, bookmarks, and annotations are preserved. Incremental updates keep signatures valid on unmodified pages.

🎯

Multi-Target Framework

Targets .NET Standard 2.0 (Framework 4.6.1+, Core 2.0+) and .NET 8/9/10+ with optimized builds and digital signature support.

Batch Processing

Process multiple find/replace pairs in a single pass. Built for high-throughput scenarios with thousands of documents.

🔎

Regex Support

Full .NET regex engine for pattern-based find and replace. Redact SSNs, phone numbers, emails, or any custom pattern.

🖥

Cross-Platform

Runs on Windows, Linux, and macOS. Deploy to Azure, AWS, Docker, or on-premises servers without platform-specific dependencies.

📦

Small Footprint

Under 500 KB total. No bloated runtime, no embedded browser engine. Minimal memory usage even on large documents.

📋

PDF Merge

Combine multiple PDFs into one document, preserving page dimensions and resources.

PDF Split

Extract individual pages or page ranges into separate PDFs. Split to files with naming patterns.

🔧

PDF Builder

Create PDFs from scratch with a fluent API. Add text, images, lines, and rectangles with full formatting control.

📝

Text Extraction

Pull text content from PDF pages. Extract from all pages or specific page ranges.

🔍

Document Inspector

Read metadata, fonts, page dimensions, and form field counts. Works without any license.

🖼

Image Replacement

Find, analyze, and replace images in PDFs. Swap logos or graphics by index or page range with JPEG/PNG.

📑

Auto-Layout Builder

Create reports with auto-pagination, text wrapping, tables, headers/footers, and page numbers.

📝

Form Filling

Read and fill AcroForm fields including text, checkbox, and dropdown. Lossless form preservation.

🚫

Redaction

Text-based, regex pattern, or area-based redaction. Permanently remove sensitive content from PDFs.

📈

Optimization

Compress streams, remove duplicate objects, and reduce file size while preserving document quality.

🔐

Digital Signatures

Sign PDFs with X.509 certificates and verify existing signatures. Available on .NET 8, 9, 10+.

🔒

Encryption & Security

AES-256 encryption with user/owner passwords. Granular permissions (print, copy, edit, annotate, forms). Decrypt to remove protection.

PDF/A Compliance

Validate and convert to PDF/A (1b, 2b, 2u, 3b, 3u) for long-term archival. Validation works without a license.

💧

Watermark

Text watermarks with positioning (top/bottom/center/across), font size, color, opacity, and per-page-range application.

📋

Bates Numbering

Stamp sequential Bates numbers across legal production sets. Prefixes, digit padding, six-position placement, and confidentiality labels. (.NET)

🔄

Async API

All I/O operations have async overloads with CancellationToken support for scalable applications.

Simple, Transparent Pricing

Annual Subscription
$499
auto-renews yearly / cancel anytime
  • Unlimited pages per document
  • Unlimited files per project
  • All features included
  • Email support from the developer
  • Automatic annual renewal

Start with a complimentary 14-day trial. No credit card required. Just install the NuGet package and call ExisLicense.Initialize() with no arguments.

Start with a complimentary 14-day trial. No credit card required. Just pip install exis-pdfeditor and call exis_pdfeditor.initialize() with no arguments.

All prices in USD. License keys are delivered via email within minutes of purchase. Volume discounts available for 5+ developers — contact support@exisone.com.

How the Complimentary Trial Works

Install & Initialize

  • Install via NuGet: dotnet add package Exis.PdfEditor
  • Call ExisLicense.Initialize() with no arguments
  • 14-day trial starts automatically
  • No registration, no credit card, no email required

Install & Initialize

  • Install via pip: pip install exis-pdfeditor
  • Call exis_pdfeditor.initialize() with no arguments
  • 14-day trial starts automatically
  • No registration, no credit card, no email required

Trial Limitations

  • Maximum 3 pages processed per document after expiry
  • Console message printed on each operation
  • All API features are fully available
  • No watermarks added to output
  • Trial resets if you clear local app data

Upgrade to Licensed

  • Purchase a license key above ($499/year)
  • Pass your key: ExisLicense.Initialize("YOUR-KEY")
  • Unlimited pages, no console messages
  • Same API, same code — just add your key

Upgrade to Licensed

  • Purchase a license key above ($499/year)
  • Pass your key: exis_pdfeditor.initialize("YOUR-KEY")
  • Or set EXIS_PDF_LICENSE_KEY environment variable
  • Unlimited pages, same API — just add your key

No code changes needed when upgrading — just add your license key to the Initialize() call.

Built by a Team You Can Trust

USA-based company. Exis LLC is incorporated in New Jersey, USA. Your license fees stay onshore and support American software development.
Government-approved vendor. Exis LLC is a registered vendor with U.S. federal and state government procurement systems (SAM.gov). Trusted by government agencies.
15+ years of experience. The team behind Exis.PdfEditor has been building document processing tools since 2010. Battle-tested across millions of documents.
Direct developer support. No ticket queues or chatbots. Email support@exisone.com and get a response from the engineer who wrote the code. Usually within 24 hours.

Frequently Asked Questions

.NET: Exis.PdfEditor targets .NET Standard 2.0 (.NET Framework 4.6.1+, .NET Core 2.0+, .NET 5-7) plus optimized builds for .NET 8, 9, and 10+ that include digital signature support. Cross-platform on Windows, Linux, and macOS. Zero native dependencies.

Python: Python 3.9 or later. Native AOT-compiled binary bundled in the wheel — no .NET runtime required. Platform wheels for Windows x64, Linux x64, and macOS ARM64 (Apple Silicon).
Both wrappers expose the same engine and the same set of capabilities — find & replace, merge, split, forms, redaction, watermarks, encryption, signatures, PDF/A, and more. The API surface follows each language's idioms (PascalCase classes in C#, snake_case functions in Python) but the underlying behavior is identical. Pricing is the same: $499 per developer per year for either language.
.NET: Install the NuGet package and call ExisLicense.Initialize() with no arguments. The trial starts automatically — no registration, no credit card, no email.

Python: Run pip install exis-pdfeditor and call exis_pdfeditor.initialize() with no arguments. Same trial, same automatic activation.

During the trial, all features are fully available with no restrictions. After expiry, you can still use the library on documents up to 3 pages.
PDFs store text as sequences of operators in content streams (e.g., Tj, TJ, '). Exis.PdfEditor parses these streams, locates the target text across operator boundaries, and performs surgical replacements at the byte level. The rest of the document — forms, signatures, metadata, fonts — remains completely untouched. Most other libraries convert the PDF to an intermediate format (HTML, DOM, or image), modify that, then rebuild the entire PDF, which destroys forms, signatures, and spacing.
Yes. Exis.PdfEditor is a pure .NET library with no UI dependencies, no native DLLs, and no system-level installs. It works in ASP.NET, Azure Functions, AWS Lambda, Docker containers, Windows Services, and any headless environment. The license is per-developer, not per-server, so you can deploy to as many servers as needed.
Each license key is tied to one developer and is valid for one year from the date of purchase. You can use the key in unlimited projects and deploy to unlimited servers. When the year expires, you need to renew to continue receiving updates and using the library. Volume discounts are available for teams of 5+ developers — contact support@exisone.com.
Yes. Exis.PdfEditor can open and process password-protected PDFs. Pass the password as an optional parameter when loading the document. The library supports both user passwords (for opening) and owner passwords (for permissions). Encrypted PDFs are decrypted in memory during processing.
Exis.PdfEditor includes an adaptive text fitting engine. When replacement text is longer, the engine can apply horizontal scaling (condensing) and slight font size reduction to fit the text in the same space. You control the limits via MinHorizontalScale and MaxFontSizeReduction options. When replacement text is shorter, the original spacing is preserved naturally. The TextFittingMode.Adaptive setting provides the best balance of quality and fit.
Yes. PdfInspector.Inspect() and PdfAConverter.Validate() work without any license or trial. You can read page counts, metadata, font lists, form field counts, and run PDF/A validation completely free. Only operations that modify PDFs (find/replace, merge, split, etc.) require a license or active trial.
We offer a full refund within 30 days of purchase if the library does not meet your needs. Since we provide a complimentary 14-day trial with full API access, we recommend thoroughly testing before purchasing. Contact support@exisone.com for refund requests.
Email support@exisone.com directly. You will reach the engineer who wrote the library — no ticket queues, no chatbots. Most inquiries are answered within 24 hours on business days. For bug reports, include a minimal code sample and the problematic PDF if possible. You can also browse the sample apps on GitHub: .NET samples or Python samples.

Start Building with Exis.PdfEditor Today

dotnet add package Exis.PdfEditor

Start Complimentary Trial on NuGet Sample App on GitHub
pip install exis-pdfeditor

Start Complimentary Trial on PyPI Sample App on GitHub

Questions? Email support@exisone.com — you will hear back from the developer who wrote the code.