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

Find & replace text in PDFs (with forensic-grade de-identification — the original text is permanently removed from the file, not just hidden), merge, split, build, fill forms, redact, sign, optimize, classify scanned vs. digital pages, and convert to PDF/A — all from your C# code. Zero dependencies. Lossless when you want it; destructive when you need it. Find & replace text in PDFs (with forensic-grade de-identification — the original text is permanently removed from the file, not just hidden), merge, split, build, fill forms, redact, sign, optimize, classify scanned vs. digital pages, 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 (fast) or finalize via PdfOptimizer
Identical PDF (only text changed) — or a single clean revision with the original permanently removed

Find & replace that actually removes the text — not just hides it

Most "PDF redaction" tools draw a black box, paint a white rectangle, or hide text behind an annotation. The original characters stay in the file's byte stream and any forensic tool can recover them. This is the root cause of nearly every public PDF redaction failure you've read about — from the 2016 court filing to ongoing FOIA disclosures.

Exis.PdfEditor edits the text-showing operators inside the content stream directly — the replaced characters are genuinely gone from the page content, with no hidden overlay to peel back. By default PdfFindReplace.Execute uses an incremental update (fast, smaller output), which keeps the original bytes as a prior revision in the file. That's normal PDF revision history — and for de-identification it is not enough.

To produce a single clean revision with the original permanently dropped, finalize with PdfOptimizer.Optimize. After this step the original text cannot be recovered from the file bytes by any forensic tool, content-stream walker, hex editor, or PDF revision-history utility — the bytes do not exist in the file. See the Destructive Find & Replace code sample below.

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.")

Destructive Find & Replace (De-identification — No Recoverable Original)

By default, PdfFindReplace.Execute writes an incremental update — fast, smaller output, but the original bytes survive as a prior revision in the file. For de-identification (PHI, PII, account numbers), follow up with PdfOptimizer.Optimize to collapse the file to a single clean revision. After this step, the original value cannot be recovered by any forensic tool. Verify by counting %%EOF markers in the output — 1 means a single clean revision; 2+ means prior content is still present.

// 1) Replace the sensitive text.
PdfFindReplace.Execute("input.pdf", "tmp.pdf", "John Smith", "PT-0047");

// 2) Collapse to a single clean revision — the pre-replacement text is dropped.
PdfOptimizer.Optimize("tmp.pdf", "safe-to-send.pdf",
    new PdfOptimizeOptions { RemoveDuplicateObjects = true });

// 3) Optional: verify the output is a single revision (no prior content retained).
int revisions = System.Text.RegularExpressions.Regex.Matches(
    System.Text.Encoding.ASCII.GetString(File.ReadAllBytes("safe-to-send.pdf")),
    "%%EOF").Count;
// 1 = clean   |   2+ = prior revisions still present

Why PdfOptimizer.Optimize rather than disabling incremental update? Optimize rebuilds through the document writer, which reproduces embedded subset fonts faithfully. A raw non-incremental rewrite can corrupt subset font encodings. Optimize is the supported way to flatten to one clean revision for sharing.

Destructive Find & Replace (De-identification — No Recoverable Original)

By default, find_replace writes an incremental update — fast, smaller output, but the original bytes survive as a prior revision in the file. For de-identification, follow up with optimize to collapse the file to a single clean revision. After this step the original value cannot be recovered by any forensic tool.

# 1) Replace the sensitive text.
exis_pdfeditor.find_replace("input.pdf", "tmp.pdf", "John Smith", "PT-0047")

# 2) Collapse to a single clean revision - the pre-replacement text is dropped.
exis_pdfeditor.optimize("tmp.pdf", "safe-to-send.pdf",
                        remove_duplicate_objects=True)

# 3) Optional: count %%EOF markers in the output (1 = clean, 2+ = revisions remain).
with open("safe-to-send.pdf", "rb") as f:
    revisions = f.read().count(b"%%EOF")

Classify Page Content (No License Required)

Detect whether each page is real text, a scan, an already-OCR'd scan, or empty. Pure content-stream analysis — no OCR engine or native dependencies. Useful for routing work (only scanned pages need image redaction/OCR) and for deciding whether text find/replace can possibly succeed.

IReadOnlyList<PageContentInfo> pages = PdfInspector.AnalyzePages("input.pdf");
foreach (var p in pages)
    Console.WriteLine($"Page {p.PageNumber}: {p.Kind} " +
        $"(text chars={p.TextCharCount}, image={p.ImageCoverageRatio:P0}, ocrLayer={p.HasInvisibleTextLayer})");

// Roll up to a single document kind: Digital | Scanned | AlreadyOcrd | Mixed | Empty
DocumentContentKind kind = PdfInspector.ClassifyDocument("input.pdf");
if (kind is DocumentContentKind.Scanned or DocumentContentKind.Mixed)
    Console.WriteLine("Has scanned pages - text find/replace won't reach image text.");

Classify Page Content (No License Required)

Detect whether each page is real text, a scan, an already-OCR'd scan, or empty — pure content-stream analysis, no OCR engine required.

pages = exis_pdfeditor.analyze_pages("input.pdf")
for p in pages:
    print(f"Page {p.pageNumber}: {p.kind} "
          f"(text chars={p.textCharCount}, image={p.imageCoverageRatio:.0%}, "
          f"ocrLayer={p.hasInvisibleTextLayer})")

# Roll up to a single document kind
kind = exis_pdfeditor.classify_document("input.pdf")
# "Digital" | "Scanned" | "AlreadyOcrd" | "Mixed" | "Empty"

Add an Invisible Searchable Text Layer

Overlay selectable, searchable text on a page using an embedded TrueType font (drawn in text render mode 3 so it is invisible). Useful for accessibility or making image-only content findable — the page's existing visible content is preserved.

byte[] ttf = File.ReadAllBytes("DejaVuSans.ttf");  // any TrueType font with the glyphs you need
byte[] output = PdfTextLayer.AddInvisibleTextLayer(
    File.ReadAllBytes("scan.pdf"), pageNumber: 1, items: new[]
    {
        new PositionedText { Text = "Patient Name: Nguyễn Phạm", X = 72, Y = 700, FontSize = 12 }
    }, ttf);
File.WriteAllBytes("searchable.pdf", output);

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

// For de-identification across many pairs (PHI, account numbers, etc.),
// chain a single PdfOptimizer.Optimize at the end to drop every superseded
// revision in one pass — producing a clean, forensically-safe output.
PdfOptimizer.Optimize("report-final.pdf", "report-clean.pdf",
    new PdfOptimizeOptions { RemoveDuplicateObjects = true });

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

// If this is a real redaction (not just a placeholder swap), follow with
// PdfOptimizer.Optimize so the original phone numbers cannot be recovered
// from prior file revisions. See the "Destructive Find & Replace" sample above.

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.

🛡

Forensic-Grade De-identification

Find/replace + PdfOptimizer.Optimize produces a single clean revision — the original text is permanently removed from the file bytes. Not hidden, not overlaid: gone. Survives hex-editor and content-stream extraction checks.

📊

Content Classification

Per-page detection of Digital, Scanned, AlreadyOcrd, or Empty content via PdfInspector.AnalyzePages / ClassifyDocument. Pure content-stream analysis, no OCR engine, no license required.

📂

Invisible Searchable Text Layer

Overlay selectable text on any page (Unicode-safe, embedded TrueType) so scans become findable while the visible image stays intact. Built for accessibility and post-OCR layering workflows.

📂

XMP + /Info Metadata

Read, replace, or wipe both modern XMP packets and the legacy /Info dictionary. Extraction is license-free and works on encrypted PDFs.

💬

Diagnostic Structure Dump

When a file fails to process and you can't share it, PdfInspector.DumpStructure emits a self-contained text report (objects, streams, filter chains, encryption details, undecodable streams) that you can paste into a bug report.

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.
Both behaviors are supported — you choose per call.

By default (UseIncrementalUpdate = true), PdfFindReplace.Execute writes an incremental update: the new content is appended to the file and the original bytes are kept as a prior revision. This is fast, produces a smaller diff, and is how PDF revision history works. The original text is no longer rendered by any viewer — but it is still present in the file bytes and can be extracted by forensic tools or by walking earlier xref tables. For most editing scenarios (fixing typos, updating dates, rewording paragraphs) this is exactly what you want.

For de-identification (PHI, account numbers, names, anything sensitive that must not survive in the file bytes), follow the find/replace with PdfOptimizer.Optimize. It rebuilds the document keeping only the objects the final revision references, discarding every superseded original. After this step the file contains exactly one revision (one %%EOF marker) and the original text cannot be recovered by any forensic tool, hex editor, content-stream walker, or PDF revision-history utility — the bytes do not exist in the file. This is the pattern used internally by the PDF Batch Editor desktop app's HIPAA-compliant find/replace workflow.

See the Destructive Find & Replace code sample for the canonical two-line pattern. To verify a file is clean, count %%EOF markers — one means a single clean revision; two or more means prior revisions are retained.

Why PdfOptimizer.Optimize rather than just disabling incremental update? Optimize rebuilds through the document writer, which reproduces embedded subset fonts faithfully. A raw non-incremental rewrite can corrupt subset font encodings, producing garbled glyphs in the output. Optimize is the supported way to flatten a document to one clean revision.
Yes. PdfInspector.AnalyzePages classifies every page as Digital (extractable text), Scanned (image with little or no text layer), AlreadyOcrd (image with an invisible OCR text layer), or Empty. PdfInspector.ClassifyDocument rolls those up to a single document kind (Digital, Scanned, AlreadyOcrd, Mixed, or Empty).

This is pure content-stream analysis — no OCR engine and no native dependencies — and it is license-free. Use it to route work efficiently (only scanned pages need image redaction or OCR) and to decide whether a text find/replace operation can possibly match anything before you spend cycles on it. See the Classify Page Content sample below.
Yes. Several read-only APIs work without any license or trial: PdfInspector.Inspect (page counts, metadata, fonts, form field counts), PdfInspector.AnalyzePages / ClassifyDocument (Digital / Scanned / AlreadyOcrd / Empty per page), PdfInspector.DumpStructure (full diagnostic dump for bug reports), PdfAConverter.Validate (PDF/A compliance check), PdfSecurity.GetEncryptionInfo, and PdfXmpMetadata.Extract (XMP + /Info read, safe on encrypted files). 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.