Exis.PdfEditor
4.8.3
dotnet add package Exis.PdfEditor --version 4.8.3
NuGet\Install-Package Exis.PdfEditor -Version 4.8.3
<PackageReference Include="Exis.PdfEditor" Version="4.8.3" />
<PackageVersion Include="Exis.PdfEditor" Version="4.8.3" />
<PackageReference Include="Exis.PdfEditor" />
paket add Exis.PdfEditor --version 4.8.3
#r "nuget: Exis.PdfEditor, 4.8.3"
#:package Exis.PdfEditor@4.8.3
#addin nuget:?package=Exis.PdfEditor&version=4.8.3
#tool nuget:?package=Exis.PdfEditor&version=4.8.3
Exis.PdfEditor
Comprehensive PDF toolkit for .NET — find/replace, merge, split, build, form filling, redaction, optimization, digital signatures, and PDF/A compliance. Operates directly on PDF content streams with zero external dependencies.
Platform Compatibility
| Build | Use with |
|---|---|
| netstandard2.0 | .NET Framework 4.6.1+, .NET Core 2.0+, .NET 5, .NET 6, .NET 7 |
| net8.0 | .NET 8, .NET 9, .NET 10+ (optimized, adds digital signature support) |
NuGet automatically selects the correct build for your project. .NET 9 and .NET 10 projects use the net8.0 build with full feature support. No additional configuration required.
dotnet add package Exis.PdfEditor
Samples: github.com/exisllc/Exis.PdfEditor-Samples
Features
- Find & Replace — regex, case-insensitive, whole-word matching with width-aware text fitting, replacement text styling (color, highlight, bold, underline, strikethrough); hyperlink targets (/URI actions in link annotations, bookmarks, OpenAction) are updated in the same pass
- Language Support — finds text in any script the document maps to Unicode (including matches spanning font changes); replacement text covers Latin, Vietnamese, Greek, Cyrillic, Hebrew, and CJK — see Language & Script Support
- Merge — combine multiple PDFs with optional page range selection
- Split — extract individual pages or page ranges into separate PDFs, or split by the document outline (table of contents) into one file per bookmark, with configurable file naming
- Build — create PDFs from scratch with absolute positioning or auto-layout (tables, pagination, headers/footers)
- Form Filling — read and fill AcroForm fields (text, checkbox, radio, dropdown/listbox), intelligent display labels from nearby page text, flatten forms to static PDF
- Signature Images — place a signature image (JPEG/PNG, transparency preserved) into signature fields, with aspect-fit or stretch scaling and alignment control
- Redaction — remove sensitive text or areas with black rectangles (regex supported)
- Region Editor — erase everything drawn inside a rectangle (vector paths, text, images — in page streams and Form XObjects), then optionally drop an image and/or text in its place; reaches logos and headings that a converter turned into vector outlines
- Outlined Text — find text that a converter flattened to vector paths (OCR on the page's own geometry, any engine via
IOcrTextRecognizer), recover the typeface it was set in by outline matching, and replace it in place — redrawn as outlines so the page stays all-vector, or as real embedded-font text; a longer replacement moves the rest of its line, uses the white paper beside it, or wraps onto the lines below (\nfor explicit breaks) - Vector Graphics — list the logos and marks that exist only as vector paths, each with a colour thumbnail, occurrence counts and a shape signature, and swap any of them for a JPEG/PNG — the raster image list, extended to artwork that has no image XObject
- Image Editor — find all images with displayable bytes (JPEG/BMP), replace all or specific images with JPEG/PNG, configurable scaling (match original, keep replacement size, scale to fit); soft and stencil masks are flagged so image lists meant for people can hide them
- Watermark — add text watermarks with configurable position, font size, color, opacity, and page selection
- Bates Stamping — sequential page numbering for legal production (prefix/suffix, configurable digit width, any corner/edge, optional confidentiality label, continuous numbering across batches, XMP audit metadata)
- QR Codes & Barcodes — stamp QR codes (versions 1-40, error correction L/M/Q/H) or Code 128 barcodes as crisp vector paths; per-page content from tokens (
{FileName},{Page},{PageCount},{Bates},{Date},{InputSha256}, your own{Column}values); six corner/edge positions plus center and custom X/Y; CropBox and/Rotateaware (always upright); print-size validation (minimum module size, capacity); contrast checks; captions; encrypted input re-encrypted the same way; PDF/A safe;{{QR:Name}}template placeholders for mail merge;AddQrCodeon both builders; paired QR beside Bates numbers - Page Editing — rotate, crop, reorder, insert blank pages, delete pages
- Stamping — overlay or underlay a PDF on top of/behind another PDF's pages, with opacity control
- Encryption — decrypt password-protected PDFs (AES-256, AES-128, RC4-128), encrypt with password and permission control
- Optimization — rebuild to a single clean revision, object streams, compress streams, deduplicate objects, strip metadata, downsample images (Flate and JPEG, with a built-in JPEG codec)
- Digital Signatures — sign with X.509 certificates (invisible or visible), verify single or all signatures, certificate metadata (net8.0+)
- PDF/A Compliance — validate and convert to PDF/A-1b, 2b, 2u, 3b, 3u
- Extract — pull text content with optional position and font metadata
- Inspect — read metadata, fonts, page dimensions, or just a fast page count (no license required)
- Content Classification — detect per page whether it is digital text, a scanned image, already-OCR'd, text drawn as vector outlines, or empty — pure content-stream analysis, no OCR or native dependencies (no license required)
- Text Layer — add an invisible, selectable text layer over a page with an embedded Unicode font (accessibility, or making scanned content findable)
- Metadata — read, replace, or wipe both modern XMP (Catalog
/Metadata) and legacy/Infodictionary; extraction works on encrypted PDFs and requires no license - Async — all I/O APIs have async overloads with CancellationToken support
- Zero dependencies — no iTextSharp, no PDFsharp, no Aspose
- Lossless — preserves form fields, checkboxes, digital signatures, layout
Quick Start
using Exis.PdfEditor;
using Exis.PdfEditor.Licensing;
// Start a 14-day trial — no key required - no limits
ExisLicense.Initialize();
// When you're ready, pass your activation key after the end of evaluation period:
// ExisLicense.Initialize("XXXX-XXXX-XXXX-XXXX");
Find & Replace
var result = PdfFindReplace.Execute("input.pdf", "output.pdf", "old text", "new text");
Console.WriteLine($"Replaced {result.TotalReplacements} occurrences on {result.PagesModified} pages");
// Hyperlink targets are updated too (on by default): when the search text is a URL,
// both the visible text AND the underlying /URI actions change — link annotations,
// bookmarks, the document OpenAction, and the catalog's base URI (/Base). The URI is
// matched independently of the visible text (display text "Click here" over a URL
// still gets its link updated).
var links = PdfFindReplace.Execute("input.pdf", "output.pdf",
"old.example.com", "new.example.com");
Console.WriteLine($"Link targets updated: {links.LinkTargetsUpdated}");
// Opt out: new PdfFindReplaceOptions { UpdateLinkTargets = false }
// Style replacement text with color, highlight, and decorations
var styled = PdfFindReplace.Execute("input.pdf", "output.pdf", "old text", "new text",
new PdfFindReplaceOptions
{
ReplacementTextColor = PdfColor.Red, // Red replacement text
ReplacementHighlightColor = PdfColor.Yellow, // Yellow background highlight
ReplacementBold = true, // Faux bold (fill + stroke)
ReplacementUnderline = true, // Underline below text
ReplacementStrikethrough = true // Strikethrough line
});
// Stream-based (for in-memory processing)
var input = new MemoryStream(File.ReadAllBytes("input.pdf"));
var output = new MemoryStream();
PdfFindReplace.Execute(input, output, "old text", "new text",
new PdfFindReplaceOptions
{
CaseSensitive = false,
TextFitting = TextFittingMode.Adaptive,
ReplacementTextColor = PdfColor.Red,
ReplacementBold = true,
ReplacementUnderline = true
});
File.WriteAllBytes("output.pdf", output.ToArray());
// Multiple replacements processed sequentially
// NOTE: Styling options must be set on EACH call's options — they don't persist.
var pairs = new[] { ("[Company]", "Acme Inc"), ("[Date]", "3/26/2026"), ("[City]", "Anytown") };
byte[] current = File.ReadAllBytes("template.pdf");
foreach (var (search, replace) in pairs)
{
var inp = new MemoryStream(current);
var outp = new MemoryStream();
PdfFindReplace.Execute(inp, outp, search, replace,
new PdfFindReplaceOptions
{
CaseSensitive = false,
TextFitting = TextFittingMode.Adaptive,
ReplacementTextColor = PdfColor.Red
});
current = outp.ToArray();
}
File.WriteAllBytes("filled.pdf", current);
Language & Script Support
Finding text works for any script the document's fonts map to Unicode. The engine
decodes ToUnicode CMaps, embedded font cmaps (Identity-H CIDFonts), standard encodings,
and /Encoding /Differences glyph names (full Adobe Glyph List). A match may span font
changes mid-phrase — common in Vietnamese documents (accented characters drawn from a
separate font) and in mixed Latin/CJK text.
Replacement text renders in the document's original font when it has the glyphs; otherwise the library embeds fallback glyphs automatically:
| Script | Replacement support |
|---|---|
| Latin (Western/Central European) | Full — embedded metric-compatible fallback fonts (Arimo/Tinos/Cousine, Apache-2.0) |
| Vietnamese (Latin Extended), Greek, Cyrillic | Full — embedded extended fallback subsets |
| Hebrew | Base letters with right-to-left ordering (no niqqud vowel points) |
| Chinese, Japanese, Korean | Full — a host system font is located and subsetted at write time; only the glyphs actually used are embedded (a few KB, output stays self-contained) |
| Arabic, Thai, Indic scripts | Not supported for replacement text (contextual shaping / mark positioning not implemented) |
For CJK, fonts are discovered automatically (Windows system fonts; Noto CJK on Linux).
PdfFindReplaceOptions.CjkFontPath / CjkFontIndex override the choice — use them for
Traditional-only Chinese documents, or on macOS where the system CJK fonts are
CFF-outline (point CjkFontPath at any TrueType .ttf/.ttc CJK font). If a required
glyph can't be sourced, the operation still completes and result.Warnings explains
what was missing.
Destructive Find & Replace (De-identification)
When you replace text to permanently remove sensitive content before sharing a document (names, account numbers, PHI), two things must be true — and there's a PDF "gotcha" that catches most tools.
1. The replacement overwrites the text — it isn't drawn over it. Exis.PdfEditor edits the text-showing operators inside the content stream directly, so the replaced characters are genuinely gone from the page content. There is no hidden overlay to peel back.
2. The original must not survive as a prior revision. By default (UseIncrementalUpdate = true), find/replace writes an incremental update: the new content is appended and the original bytes are kept in the file (this is fast, and is how PDF revision history works). The pre-replacement text is therefore still present and recoverable with forensic tooling, even though no viewer displays it. For de-identification, that is not safe to send.
To produce a single clean revision with the original dropped, finish with PdfOptimizer.Optimize. It walks the object graph from the trailer and rebuilds the document from what it reaches, so superseded content streams (including the old members of a page's /Contents array), prior cross-reference sections and every other unreferenced object are left behind:
// 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 });
After this step the original value cannot be recovered from the file bytes, and the output is a single-revision PDF.
Why
PdfOptimizer.Optimizerather than disabling incremental update? Optimize rebuilds through the document writer, which reproduces embedded subset fonts faithfully. It is the supported way to flatten a document to one clean revision for sharing.
Verify a document is clean. A single, non-incremental revision contains exactly one %%EOF marker. Two or more means incremental updates are present and earlier revisions — possibly with the original text — are retained:
int revisions = System.Text.RegularExpressions.Regex.Matches(
System.Text.Encoding.ASCII.GetString(File.ReadAllBytes("safe-to-send.pdf")), "%%EOF").Count;
// 1 = single clean revision | 2+ = prior content retained
For thorough verification, decode every stream and confirm the term is absent (born-digital text is usually FlateDecode-compressed, so a raw byte search alone can miss it), and re-extract with PdfTextExtractor.
Scanned (image-only) pages. Text find/replace only reaches real text. If the sensitive value is part of a scanned image it lives in pixels, not text — replacing a text layer would leave it visible. Remove it from the raster itself (area redaction with PdfRedactor, or burn the pixels), then finalize with PdfOptimizer.Optimize as above. Use PdfInspector.ClassifyDocument / AnalyzePages (below) to detect which pages are scanned before you start.
Merge PDFs
byte[] merged = PdfMerger.Merge(new[] { "file1.pdf", "file2.pdf", "file3.pdf" });
File.WriteAllBytes("merged.pdf", merged);
// Or merge to file directly
PdfMerger.MergeToFile(new[] { "file1.pdf", "file2.pdf" }, "merged.pdf");
// Merge with page range selection
byte[] selected = PdfMerger.Merge(new[]
{
new PdfMergeInput(File.ReadAllBytes("doc1.pdf"), new[] { 1, 3, 5 }),
new PdfMergeInput(File.ReadAllBytes("doc2.pdf")) // all pages
});
Split PDF
// Split into individual pages
List<byte[]> pages = PdfSplitter.Split("input.pdf");
// Extract specific pages (1-based)
byte[] subset = PdfSplitter.ExtractPages("input.pdf", new[] { 1, 3, 5 });
// Split to individual files
PdfSplitter.SplitToFiles("input.pdf", "page_{0}.pdf");
Split by Bookmarks (Table of Contents)
Split a single PDF into one file per bookmark, using the document outline to decide where each section begins — the way a scanned multi-part document (an orchestral score split into instrument parts, a combined statement split per account, a manual split per chapter) is broken back into separate files. Each split point's section runs from its start page up to the page before the next split point; the last section runs to the end. Any pages before the first bookmark are kept with the first section, so nothing is lost.
// Inspect the outline first (no page limit; reflects whatever bookmarks the PDF contains)
List<PdfBookmark> toc = PdfSplitter.GetBookmarks("score.pdf");
foreach (var b in toc)
Console.WriteLine($"{new string(' ', (b.Level - 1) * 2)}{b.Title} → page {b.StartPage}");
// Split to byte[] segments — each carries its title, page range, suggested file name, and bytes
List<PdfBookmarkSegment> sections = PdfSplitter.SplitByBookmarks("score.pdf");
foreach (var s in sections)
Console.WriteLine($"{s.FileName}: pages {s.StartPage}–{s.EndPage}");
// Or split straight to files in a folder, with a name prefix
List<string> written = PdfSplitter.SplitByBookmarksToFiles("score.pdf", @"C:\parts",
new PdfSplitByBookmarksOptions
{
FileNamePrefix = "Symphony No.5 - ", // → "Symphony No.5 - Flute.pdf"
MaxDepth = 1 // split at top-level bookmarks only (default)
});
SplitByBookmarks throws InvalidOperationException if the PDF has no usable bookmarks (no outline, or no destinations that resolve to pages). Destinations are resolved from explicit page targets, named destinations (both the legacy /Dests dictionary and the /Names name tree), and GoTo actions.
PdfSplitByBookmarksOptions: MaxDepth (deepest bookmark level used as a split boundary; 1 = top-level only, higher values produce finer-grained files — default 1), FileNamePrefix / FileNameSuffix (text wrapped around each name), IncludeTitleInFileName (default true; set false to name files section_01, section_02, … instead of by title).
PdfBookmark: Title, Level (1-based nesting depth), StartPage (1-based, or 0 if the destination could not be resolved).
PdfBookmarkSegment: Title, StartPage / EndPage (1-based, inclusive), FileName (sanitized of invalid characters and de-duplicated within the split — e.g. Horn.pdf, Horn_2.pdf), Data (the section as a standalone PDF).
Build a PDF from Scratch
byte[] pdf = PdfBuilder.Create()
.WithMetadata(m => m.Title("Report").Author("Exis"))
.AddPage(page => page
.Size(PdfPageSize.A4)
.AddText("Hello, World!", x: 72, y: 750, fontSize: 24,
options: o => o.Font("Helvetica").Bold().Color(0, 0, 0.8))
.AddText("Generated with Exis.PdfEditor", x: 72, y: 720, fontSize: 12)
.AddLine(72, 710, 523, 710, strokeWidth: 1)
.AddRectangle(72, 600, 200, 80, fill: true,
fillRed: 0.95, fillGreen: 0.95, fillBlue: 1.0)
.AddImage(imageBytes, x: 300, y: 400, width: 200, height: 150) // JPEG or PNG
.AddQrCode("https://example.com/r/42", x: 450, y: 72, sizePoints: 72,
configure: q => q.Caption = "example.com/r/42")) // vector QR, lower-left at (x, y)
.AddPage(page => page
.Size(PdfPageSize.Letter)
.AddText("Page 2", x: 72, y: 700, fontSize: 14))
.Build();
File.WriteAllBytes("output.pdf", pdf);
Build a Document with Auto-Layout
byte[] pdf = PdfDocumentBuilder.Create()
.PageSize(PdfPageSize.A4)
.Margins(72)
.WithMetadata(m => m.Title("Report").Author("Exis"))
.Header(h => h
.AddText("Quarterly Report", PdfHorizontalAlignment.Center, 12, o => o.Bold())
.AddLine())
.Footer(f => f
.AddLine()
.AddPageNumber()) // "Page 1 of 3"
.AddParagraph("Introduction", 18, o => o.Bold())
.AddSpacing(8)
.AddParagraph("This report covers Q1 results.")
.AddSpacing(12)
.AddTable(t => t
.Columns(2, 1, 1)
.HeaderRow(r => r.AddCell("Product").AddCell("Units").AddCell("Revenue"))
.AddRow(r => r.AddCell("Widget A").AddCell("1,200").AddCell("$24,000"))
.AddRow(r => r.AddCell("Widget B").AddCell("850").AddCell("$17,000")))
.AddQrCode("https://example.com/report/q1", 90, PdfHorizontalAlignment.Right)
.AddPageBreak()
.AddParagraph("Appendix", 14, o => o.Bold())
.Build();
Features: auto-pagination, text wrapping, tables with headers repeated on page breaks, headers/footers with page numbers, horizontal rules, images, QR codes / Code 128, spacing.
Form Filling
// Read form fields (includes smart display labels resolved from nearby page text)
List<PdfFormField> fields = PdfFormFiller.GetFields("form.pdf");
foreach (var field in fields)
{
string label = field.DisplayName ?? field.Name;
Console.WriteLine($"{label} ({field.Type}) = {field.Value}");
// e.g. "Single or Married filing separately (Checkbox) = "
// e.g. "First name and middle initial (Text) = John"
}
// Fill form fields
var result = PdfFormFiller.Fill("form.pdf", "filled.pdf", new Dictionary<string, string>
{
{ "FirstName", "John" },
{ "LastName", "Doe" },
{ "State", "CA" },
{ "AgreeToTerms", "Yes" } // checkbox
});
Console.WriteLine($"Filled {result.FieldsFilled} fields");
// Override alignment (default Auto honors each field's /Q quadding value)
var centered = PdfFormFiller.Fill("form.pdf", "filled.pdf", values,
new PdfFormFillOptions { TextAlignment = PdfFormFieldAlignment.Center });
// Flatten form (merge field appearances into page content, remove interactive fields)
PdfFormFiller.Flatten("filled.pdf", "flattened.pdf");
PdfFormField: Name (fully qualified field name), DisplayName (human-readable label resolved from nearby page text, or null), Type (Text/Checkbox/Radio/Dropdown/Listbox/Signature), Value, Options (for choice fields), IsReadOnly, HasDuplicateWidgets (true if this field has multiple widget kids at distinct positions — i.e., would be split into per-widget entries if SplitDuplicateWidgets were enabled; useful for showing a "duplicate sections detected — enable split mode?" hint in UIs).
Smart label detection: Many PDF forms use cryptic internal field names (e.g., f1_01, c1_1[0]). DisplayName resolves human-readable labels by analyzing nearby text on the page — checking right-of-field (checkbox/radio labels), left-of-field, above, and below. Handles multi-fragment labels split across font changes (e.g., "Single or Married filing separately" rendered as separate bold/normal text operations).
Text alignment (PdfFormFillOptions.TextAlignment): Auto (default) reads the field's /Q quadding value — 0 Left, 1 Center, 2 Right — so most forms render correctly without configuration. Pass Left, Center, or Right to force a single alignment for every filled field. Width is measured from real font metrics (Standard 14 width tables for Helvetica/Times/Courier; embedded-font glyph widths via the engine's font decoder); on fonts where no metrics can be resolved, the appearance falls back to left alignment.
Duplicate-widget split (PdfFormFillOptions.SplitDuplicateWidgets): Some templates — typically carbon-copy receipts or "two-up" forms where one logical field is rendered twice on a page — define a single AcroForm field whose /Kids array points to two widget annotations in different sections. Because PDF stores /V on the field (not per widget), the two widgets normally show the same value. Setting SplitDuplicateWidgets = true opts into a separate name per widget:
// GetFields with the flag returns one entry per widget, suffixed _1, _2, ...
// in page-reading order (top-to-bottom, left-to-right).
var fields = PdfFormFiller.GetFields("two-up-receipt.pdf", splitDuplicateWidgets: true);
// Address_1, Address_2, date_1, date_2, ...
// Fill writes a different value into each widget and auto-flattens so per-kid
// values survive in any viewer (interactive form is removed after fill).
PdfFormFiller.Fill("two-up-receipt.pdf", "filled.pdf", new Dictionary<string,string>
{
{ "Address_1", "Mr. David G Cruz" }, // top receipt
{ "date_1", "10-Jan-67" },
{ "Sum_1", "$35.00" },
{ "Pre-Licensing Course_1", "Yes" }, // checkbox per-receipt
{ "Address_2", "Ms. Claudia Morales" },// bottom receipt
{ "date_2", "22-Feb-26" },
{ "Sum_2", "$500.00" },
{ "Pre-Licensing Course_2", "Off" },
}, new PdfFormFillOptions { SplitDuplicateWidgets = true });
Splitting is triggered only when a field's kids sit at meaningfully different positions (different pages, or more than one field-height apart on the same page). Default false preserves legacy behavior: one entry per logical field, all kids show the same value.
Signature Images
Place a signature image (JPEG or PNG) into an existing signature field (/FT /Sig). PNG transparency is preserved via a soft mask, so a scanned signature on a transparent background renders cleanly over the page.
// Discover signature fields (name, page, rectangle, signed state)
var sigFields = PdfSignatureImage.GetSignatureFields("contract.pdf");
foreach (var f in sigFields)
Console.WriteLine($"{f.Name} on page {f.PageNumber}, signed: {f.IsSigned}");
// Place the image into the (first unsigned) signature field
byte[] signature = File.ReadAllBytes("signature.png");
var result = PdfSignatureImage.Fill("contract.pdf", "signed.pdf", signature);
// Target a specific field with scaling/alignment options
var options = new PdfSignatureImageOptions
{
FieldName = "EmployeeSignature",
ScaleMode = PdfSignatureScaleMode.FitPreserveAspect, // or Stretch
HorizontalAlignment = PdfSignatureHorizontalAlignment.Center,
VerticalAlignment = PdfSignatureVerticalAlignment.Middle,
Padding = 2.0, // points inset inside the field rect
FlattenAfterFill = false // true bakes the image into the page (removes ALL form fields)
};
PdfSignatureImage.Fill("contract.pdf", "signed.pdf", signature, options);
This is a visual signature only — no certificate is involved and the field's /V is never set, so the field remains cryptographically signable with PdfSigner. Fields that are already digitally signed are refused (overwriting their appearance would invalidate the signature). Note that when someone later digitally signs the field in Adobe Acrobat, Acrobat replaces the appearance with its own.
Redaction
var result = PdfRedactor.Redact("input.pdf", "redacted.pdf", new[]
{
// Text-based redaction
new PdfRedaction { Text = "CONFIDENTIAL" },
// Regex pattern (e.g., SSN)
new PdfRedaction { Text = @"\d{3}-\d{2}-\d{4}", IsRegex = true },
// Replace with alternative text
new PdfRedaction { Text = "SECRET", ReplaceWith = "[REDACTED]" },
// Area-based redaction on a specific page
new PdfRedaction { PageNumber = 3, Area = new PdfRect(100, 200, 300, 50) }
});
Console.WriteLine($"Applied {result.RedactionsApplied} redactions");
Region Editor (erase / replace a rectangle)
Some artwork cannot be reached by any find/replace: when a converter (Illustrator "Create
Outlines", InDesign, Aspose, CAD exporters) flattens a logo or a heading, it becomes plain
vector path operators — no image XObject, no text object. PdfRegionEditor works on geometry
instead of content: give it a rectangle and it removes every drawing operation inside it,
following the page's Form XObjects, then draws whatever you want there.
// Erase everything inside the rectangle on page 1.
// Coordinates are PDF points, origin bottom-left (same space as PdfRedaction.Area).
var result = PdfRegionEditor.Erase("catalog.pdf", "clean.pdf", 1,
new PdfRect(488, 342, 107, 64));
Console.WriteLine($"{result.ItemsErased} items erased ({result.PathsErased} vector sub-paths)");
// Erase, then drop a new logo and a company name in its place.
PdfRegionEditor.Replace("catalog.pdf", "rebranded.pdf", 1,
new PdfRect(488, 342, 107, 64),
new PdfRegionReplacement
{
BackgroundColor = PdfColor.White,
ImageBytes = File.ReadAllBytes("new-logo.jpg"),
ImageScaleMode = ImageScaleMode.ScaleToFit,
Text = new PdfRegionText
{
Text = "DINEE GLOBAL HONGKONG",
FontSize = 0, // 0 = auto-fit to the region
Bold = true,
HorizontalAlign = PdfRegionTextAlign.Center,
VerticalAlign = PdfRegionTextVAlign.Bottom
}
});
// Several regions, on any pages, in one incremental update.
PdfRegionEditor.Apply("in.pdf", "out.pdf", new[]
{
new PdfRegionEdit { PageNumber = 1, Region = new PdfRect(488, 342, 107, 64) },
new PdfRegionEdit { PageNumber = 7, Region = new PdfRect(60, 700, 200, 40),
Replacement = new PdfRegionReplacement { BackgroundColor = PdfColor.White } }
});
By default only items whose bounding box lies entirely inside the rectangle are removed
(RegionMatchMode.Inside); Intersects also removes anything that merely overlaps, while
skipping page backgrounds larger than MaxItemAreaFraction of the page.
Three details make it safe on real files:
- Sub-path precision. One
foperator routinely paints a whole line of outlined text and the logo beside it. Only the sub-paths inside the rectangle are removed; the rest of that same fill still renders (glyph counters — the hole of an "O" — go with their glyph). - Text keeps its layout. An erased
Tj/TJis replaced by a zero-glyph run of the same advance, so the text that follows it on the line does not shift left. - Shared Form XObjects. When the artwork lives in a form that several pages draw, the
default
SharedFormPolicy.Clonecopies the form for the edited page only — the other pages keep the original.EditInPlaceandSkipare available, both reported inWarnings.
Vector Graphics & Outlined Text
The Region Editor above erases whatever is drawn inside a rectangle you name. This section is the other half: finding outlined text by what it says, recovering the typeface it was set in, and replacing it in place — no coordinates required.
When text has been flattened to outlines there is no font resource, no Tj, no /ToUnicode —
only filled paths. So the only way to read it is to look at it: PdfVectorText groups a page's
outline paths into visual clusters, rasterizes each one, and hands the bitmap to an OCR engine
you supply through IOcrTextRecognizer. The word boxes that come back are mapped onto the paths
that drew them, so every hit carries exact vector geometry — bounds, colour, estimated size,
weight — and the precise list of drawing operations that would have to go to erase it.
Wiring up an OCR engine. Exis.PdfOcr ships the adapter:
using Exis.PdfOcr;
// Tesseract (cross-platform) — or VisionOcrProvider on macOS.
var recognizer = new OcrProviderTextRecognizer(new TesseractOcrProvider());
var hits = PdfVectorText.Find("catalogue.pdf", recognizer,
new PdfVectorTextOptions { Pages = new[] { 1 }, RenderScale = 4.0 });
foreach (var h in hits)
Console.WriteLine($"p{h.PageNumber} \"{h.Text}\" at {h.Bounds.X:0.#},{h.Bounds.Y:0.#} " +
$"~{h.EstimatedFontSize:0.#}pt{(h.LikelyBold ? " bold" : "")}");
Any engine will do — implement IOcrTextRecognizer (one method: grayscale PNG in, word boxes
out) over whatever you already use. Matching spans adjacent words on a line, so
Find(pdf, "HAO SHOU", recognizer) returns one hit even when the recognizer read two words; it
is case-insensitive and whole-word unless PdfVectorTextOptions.CaseSensitive /
.WholeWord say otherwise.
Several terms, one pass. Recognition does not depend on what is being searched for, so asking
for five phrases one call at a time reads every page five times. The IEnumerable<string>
overloads recognize once and match every term against that; each hit's MatchedFind says which
term it was:
var hits = PdfVectorText.Find("catalogue.pdf",
new[] { "TAIWAN HAO SHOU", "DONGGUAN" }, recognizer);
foreach (var h in hits)
Console.WriteLine($"p{h.PageNumber} matched \"{h.MatchedFind}\" as \"{h.Text}\"");
Whole documents. Recognition is the expensive part — a 132-page catalogue of flattened artwork takes about a minute even with a fast local engine — so a run over more than a handful of pages wants both a progress report and a way out:
var result = await PdfVectorText.ReplaceAsync("catalogue.pdf", "rebranded.pdf",
"HAO SHOU", "DINEE GLOBAL", recognizer,
new PdfVectorTextReplaceOptions
{
Progress = new Progress<PdfVectorTextProgress>(p =>
Console.WriteLine($"OCR page {p.PageNumber}/{p.PageCount} — {p.HitsSoFar} words so far")),
},
cancellationToken);
Progress is reported once per page, in page order. The token is observed between pages and
between clusters, and — for a recognizer that implements ICancellableOcrTextRecognizer, as
OcrProviderTextRecognizer does — inside the recognition call itself, where it is passed as an
argument to every call rather than written onto the recognizer; a cancelled ReplaceAsync
writes no output file. Create one recognizer for a batch and reuse it: starting an OCR engine
costs far more than recognizing a page, and one instance is safe to share between concurrent runs.
Behaviour change in 4.5: the default fit is
VectorTextFit.ShiftFollowingText— a replacement is drawn at the original's size and the rest of its line is moved out of the way. Before 4.5 the replacement was squeezed into the original's width. PassFit = VectorTextFit.ShrinkToFitfor the old behaviour.
By default SkipArtworkClusters keeps clusters that are clearly drawings — a map, a chart, a
product illustration — away from the recognizer entirely, which on that same catalogue cuts the
run by about a third. Lettering inside artwork can be missed as a result; set it to false to
recognize every cluster.
Recovering the font. PdfFontMatcher compares the drawn glyph shapes against every candidate
face — the fallbacks embedded in this library plus every TrueType font installed on the machine —
and ranks them by pixel overlap:
var matches = PdfFontMatcher.Match(hits[0]);
Console.WriteLine(matches[0]); // e.g. "Arial Black Bold (0.928)"
Above ~0.85 means the same design; 0.6–0.85 a close relative (a metric-compatible clone, or another weight of the family); below 0.5 a different design.
Replacing it. PdfVectorText.Replace puts the three together: find, match, erase the exact
outlines, and draw the new string on the same baseline, at the same size, in the same colour.
var result = PdfVectorText.Replace("catalogue.pdf", "rebranded.pdf",
"HAO SHOU", "DINEE GLOBAL", recognizer);
Console.WriteLine($"{result.Replacements} replaced");
foreach (var d in result.Details)
Console.WriteLine($"p{d.PageNumber} {d.Find} → {d.Replace} in {d.FontFamily} {d.FontStyle} " +
$"({d.FontScore:0.00}), {d.Mode}");
// Several phrases, one recognition pass, one incremental update.
PdfVectorText.Replace("in.pdf", "out.pdf",
new[] { ("HAO SHOU", "DINEE GLOBAL"), ("EST. 1998", "EST. 2026") }, recognizer);
Two ways to draw the replacement, chosen with PdfVectorTextReplaceOptions.Mode:
Outlines(default) — the new string is emitted as filled paths from the matched font's own glyphs. The page stays entirely vector, exactly like the artwork it replaces, and needs no font resource. This is what you want for logos and wordmarks.EmbeddedFont— real text with an embedded TrueType subset (Type0 / Identity-H, with a/ToUnicodemap), so the replacement is selectable, searchable and copyable. Only the glyphs actually drawn are embedded.
Placement. Align.Auto reads the hit's position on its line: flush with the line's left edge
→ left-anchored, flush right → right-anchored, otherwise centred on the original's centre.
Fitting — what happens when the replacement is not the original's width (Fit):
ShiftFollowingText(default) — draw the replacement at the original's size and move the rest of its line out of the way: right for a longer string, left again for a shorter one. The words after it keep their own curves, their own colour and their own clipping — only the coordinates of the path operators that drew them change — so a line that an outline exporter painted with a singlefcan have some of its glyphs erased, some moved and the rest left exactly as they were. This is what keeps a swapped company name the same size as the rest of its line.ShrinkToFit— leave the line alone and squeeze the replacement into the room before the next untouched word: condense horizontally down toMinHorizontalScale(80 % by default), then scale down. The room is everything up to the next thing on the line, not the original's own width: for the last word of its run, or a line that stands alone, it reaches to one word space before the next column, the next picture or piece of artwork beside the line, or else the page's right limit (below). A one-line address replaced by two addresses gets the whole line, not 2 pt lettering squeezed into the old box.PreserveWidth— squeeze into exactly the width the original occupied, whatever room the line has beside it.None— draw at the natural size and let it overlap whatever follows.
More than one line (Wrap, MaxLines, LineSpacing). A replacement far longer than the text
it replaces has somewhere better to go than into 3 pt lettering: the paper under its line.
Wrap = VectorTextWrap.Auto(default) — honour the line breaks in the replacement, and word-wrap a line that would otherwise have to be condensed pastMinHorizontalScale(80 %). The new lines are drawn at the original's size, one under the other, starting on its baseline.Explicit— honour the breaks, never wrap by itself; a long line is fitted as above.Off— every break becomes a single space and nothing wraps: the behaviour of 4.5.
Write a break as a real newline, or as the two characters \ and n — the form that survives a
CSV cell and a single-line text box. \\n is a literal backslash-n. Two breaks in a row leave one
blank line.
Automatic wrapping needs the hit to end its line: a phrase with other words after it is fitted
in place as before, since its second line would run under its own neighbours. Breaks the caller
wrote are honoured wherever the hit sits. A turned page (/Rotate) never wraps.
How far down the block may go is the free paper below the line: whatever is painted or placed
under it and overlaps it horizontally stops it, as does the page's bottom margin — except a filled
panel the text sits on, which is its background, not its floor (the panel's own bottom edge is
the floor instead, so a header block wraps inside its panel). MaxLines caps the count (1 = the
single fitted line of 4.5); LineSpacing overrides the pitch, which is otherwise measured from
the neighbouring lines of the same column when they look like body text (1.0–1.6 × the size) and
is 1.2 otherwise.
When the block needs more paper than there is, Fit decides who gives way — and, except under
None, a block is never drawn over what is under it:
ShiftFollowingText(default) — the page gives way. Everything below the block's first line moves down by the height of the lines that did not fit: paths, images and real text objects alike, and a panel the block sits on is stretched, keeping its top edge while its bottom follows the content.VerticalShiftreports how far andItemsMovedhow much. Something that straddles the line and is beside the block — a logo next to an address — is left where it is. The push is refused, with a warning naming the reason, when it would carry content past the page's bottom margin, or when a rule, a picture or a text object straddles that line inside the block's own column; the block then gives way as it does underShrinkToFit.ShrinkToFit/PreserveWidth— the block gives way and the page is left alone. It is shrunk in 5 % steps — smaller type needs fewer lines and sits closer together — down to half the original size; if that is still not enough its line breaks are merged from the last one backwards until what is left fits, and a single segment that still will not fit falls back to one fitted line. The warning says how many lines were wanted, how many fitted, and what it ended as.None— no fitting at all: the caller's lines are drawn at full size wherever they fall, over whatever is there, and nothing is said about it.
var result = PdfVectorText.Replace("in.pdf", "out.pdf",
"123 Main St, Anytown USA 12345-123",
"Physical Address: 123 Main St, Anytown USA 12345-123\nMailing Address: PO Box 1, Anytown USA 12345-123",
recognizer, new PdfVectorTextReplaceOptions { Wrap = VectorTextWrap.Auto });
Console.WriteLine($"{result.Details[0].Lines} line(s), " +
$"{result.Details[0].LineSpacingUsed:0.#} pt apart");
var result = PdfVectorText.Replace("in.pdf", "out.pdf", "HAO SHOU", "DINEE GLOBAL", recognizer,
new PdfVectorTextReplaceOptions
{
Fit = VectorTextFit.ShiftFollowingText, // the default
MaxShift = 0, // 0 = bounded by the next column / the page
ColumnGapFactor = 4.0, // a gap this many font sizes wide ends the line
PullTailLeftWhenShorter = true
});
foreach (var d in result.Details)
Console.WriteLine($"{d.Replace}: line moved {d.Shift:0.##} pt, fitted {d.Fitted}");
How far the line may travel is bounded three times: by the next column — the first horizontal
gap wider than ColumnGapFactor × the replacement's font size ends the line, and whatever is
beyond it never moves — by any image on the line, and by the page's right limit: the edge
of the page's own rightmost content (the margin the layout was made with), never less than half an
inch from the page edge and never closer than 2 pt to it. A
replacement that would push the line past that limit falls back to ShrinkToFit, and says so in
result.Warnings; so does a /Rotated page, and a replacement that is not left-anchored on its
line (only left anchoring moves the tail in this version). Details[i].Shift reports the distance
actually applied and Details[i].Fitted whether the replacement had to be squeezed after all.
Several replacements on one line are applied left to right and their shifts accumulate.
ShrinkToFit = false still works and now means Fit = VectorTextFit.None; the property is
obsolete in favour of Fit.
CJK. In Outlines mode, Chinese/Japanese/Korean replacement text is drawn from a CJK font
found on the host at write time (the same resolution PdfFindReplace uses — those families are
10–20 MB and cannot be shipped). If the machine has none, the replacement falls back to
EmbeddedFont mode and a warning is reported.
Font control. By default the typeface is matched from the outlines. Set FontFamily to force
one, Bold to override the detected weight, MatchFont = false to skip matching (the embedded
Arimo is then used), or FontMatchOptions = new PdfFontMatchOptions { EmbeddedOnly = true } to
restrict the answer to the faces this library ships — which makes the output byte-identical on
every machine.
Everything reported non-fatally — an overlapping hit skipped, a glyph the chosen face does not
have, a shared Form XObject cloned — lands in result.Warnings.
Vector Graphics (find & replace logos drawn as paths)
A logo that a converter flattened — "Create Outlines", an Aspose or CAD export — has no image
XObject at all, so PdfImageEditor.FindImages correctly reports nothing for it
and an image list built on that API has nothing to show. PdfVectorGraphics is the same list,
extended to artwork made of paths:
var graphics = PdfVectorGraphics.Find("catalogue.pdf");
foreach (var g in graphics)
{
Console.WriteLine($"#{g.Index} {g.Kind} {g.WidthPt:0.#}×{g.HeightPt:0.#} pt " +
$"on page(s) {string.Join(",", g.PageNumbers)} {g.Signature}");
File.WriteAllBytes($"graphic-{g.Index}.png", g.ThumbnailPng); // colour PNG, ≤ 256 px
}
Each entry is a cluster of painting operations that belong together visually — a mark, the ® next
to it and its wordmark come back as one graphic, because that is what a customer means by "the
logo". ThumbnailPng is rendered from the paths themselves in their own colours, so it looks like
the artwork rather than a silhouette, and DominantColors reports up to four area-weighted
colours (CMYK content is converted).
Occurrences. The same drawing on forty pages is one entry with forty PageNumbers and
OccurrenceBounds, matched by Signature — a hash of a 24×24 aspect-preserving binarised render
plus the colour set. The signature is stable across documents too, so a batch tool can group one
logo over a folder of files, and can re-find a graphic by name after an edit has renumbered the
indices. Set DeduplicateAcrossPages = false for one entry per occurrence.
Replacing. The graphic's own drawing operations are erased — not everything inside its rectangle, so a caption that overlaps the logo's box survives — and the image is drawn in the space they occupied. One image XObject is shared by every occurrence, and the whole job is a single incremental update:
var result = PdfVectorGraphics.ReplaceAll("catalogue.pdf", "rebranded.pdf",
File.ReadAllBytes("newlogo.jpg"),
new[] { 0 }, // null = every graphic
new PdfVectorGraphicReplaceOptions { ScaleMode = ImageScaleMode.ScaleToFit });
Console.WriteLine($"{result.GraphicsReplaced} graphics, " +
$"{result.OccurrencesReplaced} occurrences, {result.ItemsErased} paths erased");
// Different images for different graphics, in one pass:
PdfVectorGraphics.Replace("in.pdf", "out.pdf", new[]
{
new PdfVectorGraphicReplacement(0, logoBytes),
new PdfVectorGraphicReplacement(3, sealBytes) { ScaleMode = ImageScaleMode.MatchOriginalSize }
});
ScaleMode is PdfImageEditor's enum: ScaleToFit (default — aspect preserved, centred),
MatchOriginalSize (stretched to the bounds exactly) or KeepReplacementSize (natural size,
1 pixel = 1 point, centred). BackgroundColor fills the bounds first; EraseWholeCluster = false
removes only the paths the placed image will actually cover.
Indices come from a Find on the same document, with the same options. An index outside that
list throws ArgumentException. If the Find used non-default options, hand the very same options
object to PdfVectorGraphicReplaceOptions.FindOptions — otherwise the indices point at a
different set of graphics. In a pipeline that rewrites a file several times, re-run Find on the
current file and match on Signature rather than carrying indices across edits.
What is not listed. Page backgrounds and frames (an operation or a cluster covering more than
MaxPageFraction of the page, 0.35 by default), anything smaller than MinSizePt (12 pt) in
either direction, hairline rules, and blocks of type that were converted to outlines — an address
block is not an image. IncludeTextLike = true lists those too, labelled
VectorGraphicKind.TextLike; the other labels are Logo, Drawing, Rule and Unknown, and
they are labels only, never filters, apart from text and rules.
Image Editor
// Find all images in a PDF (includes displayable image bytes)
var found = PdfImageEditor.FindImages("input.pdf");
foreach (var img in found.Images)
{
Console.WriteLine($"Image #{img.Index}: {img.PixelWidth}x{img.PixelHeight} {img.ColorSpace} {img.Format} " +
$"on page(s) {string.Join(", ", img.PageNumbers)}");
Console.WriteLine($" Data: {img.Data.Length} bytes"); // JPEG or BMP bytes for display
}
// Display image thumbnail in WPF
var firstImage = found.Images[0];
if (firstImage.Data.Length > 0)
{
var bmp = new BitmapImage();
bmp.BeginInit();
bmp.StreamSource = new MemoryStream(firstImage.Data);
bmp.CacheOption = BitmapCacheOption.OnLoad;
bmp.EndInit();
myImageControl.Source = bmp; // Works for both JPEG and BMP data
}
// Replace all images with a new one
byte[] newLogo = File.ReadAllBytes("new-logo.jpg");
var result = PdfImageEditor.ReplaceAll("input.pdf", "output.pdf", newLogo);
Console.WriteLine($"Replaced {result.ImagesReplaced} of {result.ImagesFound} images");
// Replace specific images by index or page range
var selective = PdfImageEditor.Replace("input.pdf", "output.pdf", newLogo,
new PdfImageReplaceOptions { ImageIndices = new[] { 0, 2 } });
// Replace with scaling options
var scaled = PdfImageEditor.Replace("input.pdf", "output.pdf", newLogo,
new PdfImageReplaceOptions
{
ScaleMode = ImageScaleMode.ScaleToFit // Fit within original bounds, preserve aspect ratio
});
ImageScaleMode:
| Mode | Behavior |
|---|---|
MatchOriginalSize |
Default. Fills the exact same display rectangle as the original. May distort if aspect ratios differ. |
KeepReplacementSize |
Displays at natural size, preserving the original DPI. Larger images appear larger, smaller appear smaller. |
ScaleToFit |
Fits within the original rectangle while preserving aspect ratio. Centered, with empty space if needed. |
Watermark
// Diagonal watermark across all pages (default)
PdfWatermark.AddText("input.pdf", "output.pdf", "CONFIDENTIAL");
// Top watermark, red, 50% opacity, specific pages
PdfWatermark.AddText("input.pdf", "output.pdf", "DRAFT", new PdfWatermarkOptions
{
Position = WatermarkPosition.Top,
FontSize = 36,
TextColor = PdfColor.Red,
Opacity = 0.5,
PageRange = new[] { 1, 2, 3 }
});
Page Editing
// Rotate all pages 90 degrees clockwise
PdfPageEditor.Rotate("input.pdf", "rotated.pdf", 90);
// Rotate specific pages
PdfPageEditor.Rotate("input.pdf", "rotated.pdf", 180,
new PdfPageEditOptions { PageRange = new[] { 1, 3 } });
// Crop pages (coordinates in points: 72pt = 1 inch)
PdfPageEditor.Crop("input.pdf", "cropped.pdf", new PdfRect(72, 72, 468, 648));
// Reorder pages (1-based)
PdfPageEditor.Reorder("input.pdf", "reordered.pdf", new[] { 3, 1, 2 });
// Delete pages
PdfPageEditor.DeletePages("input.pdf", "trimmed.pdf", new[] { 2, 4 });
// Insert blank pages
byte[] result = PdfPageEditor.InsertBlankPages(data, new[]
{
new PdfBlankPageInsertion { AfterPage = 0, Size = PdfPageSize.A4 }, // before page 1
new PdfBlankPageInsertion { AfterPage = 3, Size = PdfPageSize.Letter } // after page 3
});
Stamping (PDF Overlay/Underlay)
byte[] letterhead = File.ReadAllBytes("letterhead.pdf");
// Overlay: stamp on top of existing content
PdfStamper.Overlay("input.pdf", "stamped.pdf", letterhead);
// Underlay: stamp behind existing content (like a PDF-based watermark)
PdfStamper.Underlay("input.pdf", "branded.pdf", letterhead);
// With options: specific pages, partial opacity
PdfStamper.Overlay("input.pdf", "output.pdf", letterhead, new PdfStampOptions
{
PageRange = new[] { 1 }, // first page only
StampPageNumber = 1, // use page 1 of stamp PDF
Opacity = 0.5 // 50% transparent
});
Bates Stamping
Sequential page numbering for legal production and discovery workflows. Each page receives a zero-padded identifier (e.g. ABC000001, ABC000002) rendered in a chosen corner of the visual page. Placement is relative to the page's /Rotate orientation, so mixed-rotation documents look consistent. An XMP audit block recording the range, digit width, and prefix/suffix is written to the catalog by default.
// Defaults: number starts at 1, 6 digits, bottom-right corner
var result = PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf");
Console.WriteLine($"Stamped pages {result.FirstNumber}–{result.LastNumber}");
// Prefix/suffix, custom position, color, confidentiality label
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
Prefix = "ABC",
StartNumber = 1,
Digits = 6, // → "ABC000001"
Position = BatesPosition.BottomRight,
FontSize = 10,
TextColor = PdfColor.Black,
BackgroundColor = PdfColor.White, // opaque box behind stamp
MarginInches = 0.5f,
ConfidentialityLabel = "CONFIDENTIAL" // stacked above Bates number
});
// Continuous numbering across a batch — thread LastNumber + 1 into the next call
int next = 1;
foreach (var path in docs)
{
var r = PdfBatesStamp.ApplyBatesStamp(path, path + ".stamped.pdf",
new BatesStampOptions { Prefix = "ABC", StartNumber = next });
next = r.LastNumber + 1;
}
// Skip the cover page, but keep the counter advancing (legal convention —
// the cover is "ABC000001" in the production log even if unstamped)
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
Prefix = "ABC",
SkipFirstPage = true,
CounterAdvancesOnSkippedPages = true // default
});
// Stamp only selected pages
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
PageRange = new[] { 2, 3, 5 } // 1-based
});
// Signed input: stamping invalidates signatures. Off by default — opt in.
PdfBatesStamp.ApplyBatesStamp("signed.pdf", "stamped.pdf", new BatesStampOptions
{
AllowSignedInput = true // Warnings[] will record it
});
// Suppress the XMP audit block (on by default)
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
WriteXmpMetadata = false
});
// QR code beside every Bates number: encodes the number plus a document ID.
// The code takes the corner; the number moves next to it.
PdfBatesStamp.ApplyBatesStamp("input.pdf", "stamped.pdf", new BatesStampOptions
{
Prefix = "ABC",
QrCode = new QrStampOptions
{
Content = "{Bates}|{DocId}", // default "{Bates}"
Values = new Dictionary<string, string> { ["DocId"] = "MATTER-17" },
SizePoints = 54
}
});
QR Codes & Barcodes
// One code per page, bottom-right, 1 inch, error correction M (defaults)
var result = PdfQrStamp.ApplyQrStamp("input.pdf", "output.pdf", new QrStampOptions
{
Content = "https://example.com/docs/{FileNameNoExt}?page={Page}"
});
foreach (var code in result.Codes)
Console.WriteLine($"page {code.PageNumber}: v{code.Version}, {code.ModuleSizeMm} mm modules");
// Tokens: {FileName} {FileNameNoExt} {Page} {PageCount} {Date} {InputSha256} {Bates} + your own values
PdfQrStamp.ApplyQrStamp("contract.pdf", "stamped.pdf", new QrStampOptions
{
Content = "{InvoiceId}|{InputSha256}", // hash of the ORIGINAL input bytes
Values = new Dictionary<string, string> { ["InvoiceId"] = "INV-2026-0042" },
Pages = QrPageSelection.FirstPage, // one code per document
Position = QrPosition.TopRight,
SizePoints = 90,
ErrorCorrection = QrErrorCorrection.Q,
Caption = "INV-2026-0042", // drawn as vector outlines (no font needed)
ForegroundColor = new PdfColor(0, 0, 0.35) // must be darker than the background, 4.5:1 contrast
});
// {Bates} follows the same numbering rules as PdfBatesStamp with these options
PdfQrStamp.ApplyQrStamp("input.pdf", "output.pdf", new QrStampOptions
{
Content = "{Bates}",
Bates = new BatesStampOptions { Prefix = "ABC", StartNumber = 1001 },
Position = QrPosition.BottomLeft
});
// Code 128 (ASCII only)
PdfQrStamp.ApplyQrStamp("input.pdf", "output.pdf", new QrStampOptions
{
Content = "SHIP-000123",
BarcodeType = BarcodeType.Code128,
SizePoints = 180, // total width incl. quiet zones
BarHeightPoints = 36
});
// Encrypted input: re-encrypted with the same open password and permissions
PdfQrStamp.ApplyQrStamp("locked.pdf", "out.pdf", new QrStampOptions { Content = "x", Password = "secret" });
// Will it scan? Validate before stamping (no PDF, no license)
var check = PdfQrStamp.Validate("https://example.com/a/very/long/url", new QrStampOptions { SizePoints = 36 });
if (!check.IsValid) Console.WriteLine(check.Message); // "Too small to scan reliably ... Use at least N pt."
Placeholders (mail merge). Put {{QR:Name}} in the template text; each token is removed and a code is
drawn with its top-left corner where the token began. Tokens split across text runs are found.
foreach (var row in rows) // e.g. CSV rows as dictionaries
{
PdfQrStamp.ApplyQrPlaceholders("template.pdf", $"out_{row["TicketId"]}.pdf", new QrPlaceholderOptions
{
Values = row, // {{QR:TicketId}} -> row["TicketId"]
RequireValues = true, // a misspelt column is an error, not a code containing the column name
SizePoints = 72
});
}
// {{QR:https://example.com}} with no matching value encodes the text itself.
Codes are drawn as filled vector paths on an opaque background (never transparent), so they stay sharp at
any zoom, add no fonts or images, and keep PDF/A files conformant (DeviceCMYK/DeviceGray are used when the
output intent requires it). A module smaller than 0.5 mm (QR) or 0.25 mm (Code 128) at print size is an
error unless AllowUndersized = true; QR codes of version 26+ carry a "dense code" warning. Signed input is
refused unless AllowSignedInput = true — stamp before signing.
Encryption & Decryption
// Decrypt a password-protected PDF
PdfSecurity.Decrypt("protected.pdf", "unlocked.pdf", "password");
// Encrypt with AES-256 (strongest standard encryption)
PdfSecurity.Encrypt("input.pdf", "locked.pdf", new PdfEncryptOptions
{
UserPassword = "open123", // password to open
OwnerPassword = "admin456", // password for full access
Permissions = PdfPermissions.Print | PdfPermissions.CopyText // restricted permissions
});
// Check encryption status (no license required)
var info = PdfSecurity.GetEncryptionInfo("file.pdf");
Console.WriteLine($"Encrypted: {info.IsEncrypted}, Version: {info.Version}");
Optimization
var result = PdfOptimizer.Optimize("input.pdf", "optimized.pdf", new PdfOptimizeOptions
{
CompressStreams = true, // Compress uncompressed streams (XMP metadata stays unfiltered for PDF/A)
RemoveDuplicateObjects = true, // Merge identical objects (never pages, fields, annotations or structure nodes)
RemoveMetadata = false, // true drops /Info AND the XMP stream from the file bytes
DownsampleImages = true, // Reduce images drawn above MaxImageDpi
MaxImageDpi = 150, // Measured at the size the image is drawn (default: 150)
JpegQuality = 75, // Re-encode quality for downsampled JPEGs (default: 75)
UseObjectStreams = true // Object streams + xref stream (PDF 1.5); off = classic xref
});
Console.WriteLine($"Saved {result.BytesSaved} bytes ({result.ReductionPercent:F1}%)");
Console.WriteLine($"Dead objects dropped: {result.UnreferencedObjectsRemoved}, images downsampled: {result.ImagesDownsampled}");
Optimize always rebuilds the file from the objects the current revision references: prior revisions, superseded objects, old cross-reference and object streams and linearization data are dropped, and every reference is renumbered to generation 0. Images are downsampled only when the result is genuinely smaller than the stored original. Flate and raw images are re-encoded as Flate (with a PNG predictor when that helps); JPEG images (baseline or progressive) are decoded with the library's own codec, downsampled and re-encoded as JPEG at JpegQuality — a 300 dpi scan taken to 150 dpi typically loses 60–70 % of its size. A JPEG that is not oversampled is never re-encoded. JPEG 2000, CCITT, JBIG2, arithmetic-coded JPEG, palette and colour-key-masked images are never touched.
Digital Signatures (net8.0+)
using System.Security.Cryptography.X509Certificates;
// Sign a PDF (invisible signature)
var cert = new X509Certificate2("certificate.pfx", "password");
PdfSigner.Sign("input.pdf", "signed.pdf", new PdfSignOptions
{
Certificate = cert,
Reason = "Approved",
Location = "New York",
ContactInfo = "admin@example.com"
});
// Sign with a visible signature annotation on the page
PdfSigner.Sign("input.pdf", "signed.pdf", new PdfSignOptions
{
Certificate = cert,
Reason = "Final Approval",
Location = "New York",
SignerName = "Jane Doe",
Visible = true,
PageNumber = 1,
Rectangle = new PdfSignatureRectangle(72, 50, 200, 60) // x, y, width, height in points
});
// Verify a signed PDF
PdfSignatureInfo info = PdfSigner.Verify("signed.pdf");
Console.WriteLine($"Signed: {info.IsSigned}");
Console.WriteLine($"Valid: {info.IsValid}");
Console.WriteLine($"Signer: {info.SignerName}");
Console.WriteLine($"Certificate: {info.CertificateSubject}");
Console.WriteLine($"Issuer: {info.CertificateIssuer}");
Console.WriteLine($"Timestamp: {info.HasTimestamp}");
// Verify all signatures in a document
List<PdfSignatureInfo> all = PdfSigner.VerifyAll("multi-signed.pdf");
foreach (var sig in all)
Console.WriteLine($"{sig.SignerName}: valid={sig.IsValid}");
PDF/A Compliance
// Validate (no license required)
PdfAValidationResult result = PdfAConverter.Validate("input.pdf", PdfALevel.PdfA2b);
Console.WriteLine($"Compliant: {result.IsCompliant}");
foreach (var v in result.Violations)
Console.WriteLine($" [{v.Code}] {v.Message} (auto-fix: {v.CanAutoFix})");
// Convert to PDF/A
byte[] pdfa = PdfAConverter.Convert("input.pdf", PdfALevel.PdfA2b);
File.WriteAllBytes("output-pdfa.pdf", pdfa);
Extract Text
// Simple extraction
PdfTextResult text = PdfTextExtractor.ExtractText("input.pdf");
Console.WriteLine(text.FullText);
// Extract from specific pages
PdfTextResult partial = PdfTextExtractor.ExtractText("input.pdf", new[] { 1, 3 });
// Structured extraction with position and font data
PdfStructuredTextResult structured = PdfTextExtractor.ExtractStructured("input.pdf");
foreach (var block in structured.Pages[0].TextBlocks)
Console.WriteLine($"[{block.X:F0},{block.Y:F0}] {block.Text} " +
$"(font={block.FontName}, size={block.FontSize})");
Inspect Document (No License Required)
PdfDocumentInfo info = PdfInspector.Inspect("input.pdf");
Console.WriteLine($"Pages: {info.PageCount}");
Console.WriteLine($"Title: {info.Title}");
Console.WriteLine($"Fonts: {string.Join(", ", info.FontsUsed)}");
Console.WriteLine($"Encrypted: {info.IsEncrypted}");
Console.WriteLine($"Form fields: {info.FormFieldCount}");
// Just the page count — walks the page tree only, no text parsing, so it is fast on a large
// file (for an encrypted file the root /Pages /Count is returned without the password)
int pages = PdfInspector.GetPageCount("input.pdf");
Classify Page Content (No License Required)
Determine whether each page is real text, a scan, an already-OCR'd scan, text drawn as vector outlines, or empty — useful for routing work (only scanned pages need image redaction/OCR; only outlined pages need the outlined-text pass). Pure content-stream analysis; no OCR engine or native dependencies.
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
DocumentContentKind kind = PdfInspector.ClassifyDocument("input.pdf");
// Digital | Scanned | AlreadyOcrd | Outlined | Mixed | Empty
if (kind is DocumentContentKind.Scanned or DocumentContentKind.Mixed)
Console.WriteLine("Has scanned pages — text find/replace won't reach image text.");
if (kind == DocumentContentKind.Outlined)
Console.WriteLine("Text is drawn as paths — use PdfVectorText to find and replace it.");
PageContentKind: Digital (extractable text), Scanned (image, little/no text), AlreadyOcrd (image with an invisible OCR text layer), Outlined (the visible text is filled vector paths — a "Create Outlines" export or a print-to-PDF that converted type to curves; no text search can see it, only PdfVectorText can), Empty. A page is Outlined when it has at least 40 letter-sized filled shapes and more than twice as many of them as extractable characters. PageContentInfo exposes PageNumber, Kind, TextCharCount, TextCoverageRatio, ImageCoverageRatio, HasInvisibleTextLayer, GlyphLikePathCount (filled paths 1–40 pt tall and at most 40 pt wide — one per glyph on an outlined page, a handful on an ordinary one).
Add an Invisible Text Layer
Overlay selectable, searchable text on a page using an embedded TrueType font (CIDFontType2 / Identity-H with a ToUnicode map, drawn in text render mode 3 so it is invisible). Any Unicode round-trips through find/replace and extraction — 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 needed glyphs
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);
PositionedText: Text, X, Y (PDF points, origin bottom-left), FontSize, optional TargetWidth (horizontally scales the run to span a given width).
Diagnostic Structure Dump (No License Required)
When a PDF fails to process and you can't share the file, PdfInspector.DumpStructure
produces a self-contained text report you can paste into a bug report. It walks every
object in the file, tallies filter chains and font subtypes, lists encryption details,
and records any streams that fail to decode — without needing the original file.
// Inspect a problem file and print a human-readable report
PdfStructureDump dump = PdfInspector.DumpStructure("problem.pdf");
Console.WriteLine(dump.ToString());
// Or pull individual fields programmatically
Console.WriteLine($"PDF version: {dump.Version}");
Console.WriteLine($"Pages: {dump.PageCount}");
Console.WriteLine($"Encrypted: {dump.IsEncrypted} (V={dump.EncryptionVersion}, R={dump.EncryptionRevision})");
foreach (var kv in dump.FilterChains)
Console.WriteLine($" {kv.Key}: {kv.Value} streams");
// Streams that failed to decode (capped at 50)
foreach (var bad in dump.UnsupportedStreams)
Console.WriteLine($"obj {bad.ObjectNumber} [{bad.FilterChain}]: {bad.Error}");
The ToString() output is also designed for support workflows — copy it into an email
or issue and the maintainer has everything needed to diagnose without the source file:
=== Exis.PdfEditor Structure Dump ===
File: problem.pdf
Size: 1159381 bytes
PDF version: 1.7
Pages: 12
Objects: 8505 (xref entries: 8506)
Stream objects: 1171
Xref streams: yes
--- Encryption ---
Encrypted: yes
Version (V): 5
Revision (R): 6
Key length: 256 bits
Permissions: 0xFFFFFBE4
Filter: /Standard
StmF: /StdCF
StrF: /StdCF
--- Filter chains (streams) ---
FlateDecode 1163
DCTDecode 3
--- Catalog flags ---
AcroForm: yes
Signed: no
EmbeddedFiles: no
Portfolio: no
Overloads available: DumpStructure(string path), DumpStructure(byte[] data, string? password = null),
DumpStructure(Stream stream, string? password = null), plus DumpStructureAsync with CancellationToken.
XMP + Info Metadata
PdfXmpMetadata reads, replaces, and removes both forms of PDF document metadata:
- XMP — the modern RDF/XML packet referenced from the Catalog
/Metadataentry - Info — the legacy
/Infotrailer dictionary (Title, Author, Subject, Keywords, Creator, Producer, CreationDate, ModDate, plus custom keys)
Extract is license-free and never throws on encrypted files. All mutation methods (SetXmp, RemoveXmp, SetInfo, RemoveInfo, RemoveAll) require a license and reject encrypted PDFs — decrypt first with PdfSecurity.Decrypt.
Read metadata
// No license required — works on encrypted files too (XMP only).
PdfMetadataInfo meta = PdfXmpMetadata.Extract("input.pdf");
if (meta.HasXmp)
{
Console.WriteLine($"XMP packet: {meta.XmpByteSize} bytes");
Console.WriteLine(meta.XmpXml); // Full <?xpacket ... ?> payload as UTF-8 string
}
if (meta.HasInfo)
{
PdfInfoDict info = meta.Info;
Console.WriteLine($"Title: {info.Title}");
Console.WriteLine($"Author: {info.Author}");
Console.WriteLine($"Subject: {info.Subject}");
Console.WriteLine($"Keywords: {info.Keywords}");
Console.WriteLine($"Creator: {info.Creator}");
Console.WriteLine($"Producer: {info.Producer}");
Console.WriteLine($"Created: {info.CreationDate:o}");
Console.WriteLine($"Modified: {info.ModificationDate:o}");
// Non-standard /Info keys (e.g. "Company", custom producer fields)
foreach (var kv in info.Custom)
Console.WriteLine($"[custom] {kv.Key} = {kv.Value}");
}
// Byte[] / Stream overloads are available too:
byte[] pdf = File.ReadAllBytes("input.pdf");
PdfMetadataInfo meta2 = PdfXmpMetadata.Extract(pdf);
Replace XMP packet
// Set a brand-new XMP packet. Must be a valid <?xpacket ...?> wrapped RDF/XML document.
string xmp = @"<?xpacket begin=""""?>
<x:xmpmeta xmlns:x=""adobe:ns:meta/"">
<rdf:RDF xmlns:rdf=""http://www.w3.org/1999/02/22-rdf-syntax-ns#"">
<rdf:Description xmlns:dc=""http://purl.org/dc/elements/1.1/"">
<dc:title><rdf:Alt><rdf:li xml:lang=""x-default"">Quarterly Report</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>Finance Team</rdf:li></rdf:Seq></dc:creator>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end=""w""?>";
PdfXmpMetadata.SetXmp("input.pdf", "output.pdf", xmp);
// Byte[] and Stream overloads:
byte[] updated = PdfXmpMetadata.SetXmp(File.ReadAllBytes("input.pdf"), xmp);
using (var inStream = File.OpenRead("input.pdf"))
using (var outStream = File.Create("output.pdf"))
{
PdfXmpMetadata.SetXmp(inStream, outStream, xmp);
}
Passing a null or empty string to SetXmp writes an empty XMP packet (still present, but with no data). To drop the /Metadata reference entirely, use RemoveXmp.
Replace /Info dictionary
// The document must already have an /Info reference in its trailer.
// If it doesn't, SetInfo throws InvalidOperationException — use SetXmp instead.
var info = new PdfInfoDict
{
Title = "Quarterly Report",
Author = "Finance Team",
Subject = "Q1 2026 earnings summary",
Keywords = "earnings, Q1, 2026",
Creator = "Exis.PdfEditor",
Producer = "Exis.PdfEditor 3.6",
CreationDate = new DateTime(2026, 4, 1, 9, 0, 0, DateTimeKind.Utc),
ModificationDate = DateTime.UtcNow,
};
// Preserve or add custom /Info keys (Acrobat accepts any PDF name key)
info.Custom["Company"] = "Exis LLC";
info.Custom["Revision"] = "v3.6.3";
PdfXmpMetadata.SetInfo("input.pdf", "output.pdf", info);
Fields left null on PdfInfoDict are written as missing — not as empty strings — which preserves "not set" semantics in the output.
Remove metadata (wipe for privacy)
// Drop only the XMP packet (Catalog /Metadata key removed)
PdfXmpMetadata.RemoveXmp("input.pdf", "stripped-xmp.pdf");
// Empty the /Info dict (trailer ref kept, contents cleared)
PdfXmpMetadata.RemoveInfo("input.pdf", "stripped-info.pdf");
// Drop both in one incremental update (smaller output than calling both in sequence)
PdfXmpMetadata.RemoveAll("input.pdf", "stripped.pdf");
Async overloads
Every method has an async counterpart with CancellationToken support:
PdfMetadataInfo meta = await PdfXmpMetadata.ExtractAsync("input.pdf", ct);
await PdfXmpMetadata.SetXmpAsync("input.pdf", "output.pdf", xmp, ct);
await PdfXmpMetadata.SetInfoAsync("input.pdf", "output.pdf", info, ct);
await PdfXmpMetadata.RemoveAllAsync("input.pdf", "stripped.pdf", ct);
byte[] cleaned = await PdfXmpMetadata.RemoveAllAsync(File.ReadAllBytes("input.pdf"), ct);
API Reference
PdfFindReplace
// File-based
PdfFindReplaceResult Execute(string inputPath, string outputPath,
string searchText, string replaceText, PdfFindReplaceOptions? options = null);
// Stream-based
PdfFindReplaceResult Execute(Stream input, Stream output,
string searchText, string replaceText, PdfFindReplaceOptions? options = null);
// Multiple pairs
PdfFindReplaceResult Execute(string inputPath, string outputPath,
IEnumerable<FindReplacePair> pairs, PdfFindReplaceOptions? options = null);
PdfFindReplaceResult: TotalReplacements, PagesModified, PagesSearched, Details (one
PdfReplacementDetail per replacement: PageNumber, OriginalText, and what was drawn),
LinkTargetsUpdated, Warnings, InvisibleTextLayerPages (pages where the match sat in an
invisible OCR text layer over a scan — the text changed but the scan image still shows the old
words; such pages classify as AlreadyOcrd, route them through image redaction/OCR instead).
PdfFindReplaceOptions
var options = new PdfFindReplaceOptions
{
CaseSensitive = true, // Case-sensitive matching (default: true)
WholeWordOnly = false, // Match whole words only
UseRegex = false, // Enable regex patterns
UseIncrementalUpdate = true, // Append changes, keep prior revisions (fast, smaller).
// For de-identification, finalize with PdfOptimizer.Optimize
// (see "Destructive Find & Replace") to drop the original.
PageRange = null, // Limit to specific pages (null = all)
UpdateLinkTargets = true, // Also replace inside /URI link targets: link
// annotations, bookmarks, OpenAction, and the catalog's
// /URI base-URI (/Base). With PageRange set, only that
// range's link annotations are updated. Result:
// LinkTargetsUpdated + Details (page 0 = document-level);
// counts add to TotalReplacements.
// Text fitting — controls what happens when replacement is wider than original
TextFitting = TextFittingMode.None, // None | PreserveWidth | FitToPage | Adaptive
MinHorizontalScale = 70, // Minimum Tz percentage (50-100)
MaxFontSizeReduction = 1.5, // Max font size reduction in points (Adaptive only)
Wrap = TextWrapMode.Auto, // Auto | Explicit | Off — how a replacement may
// become more than one line (see "More than one line")
MaxLines = 0, // Cap on lines per replacement (0 = what the room allows)
LineSpacing = 0, // Baseline pitch as a multiple of the size
// (0 = measured from the page's own lines, else 1.2)
EmbedFallbackFonts = true, // Characters the document's font lacks are drawn with an
// embedded fallback subset (Arimo/Tinos/Cousine, ~16 KB);
// false references an unembedded Standard-14 font instead
// Replacement text styling
ReplacementTextColor = PdfColor.Red, // Change fill color of replacement text
ReplacementHighlightColor = PdfColor.Yellow, // Draw colored rectangle behind replacement text
ReplacementBold = false, // Faux bold via fill+stroke rendering
ReplacementUnderline = false, // Draw underline below replacement text
ReplacementStrikethrough = false, // Draw strikethrough through replacement text
// CJK replacement text (see "Language & Script Support")
CjkFontPath = null, // Explicit TrueType font for CJK glyphs (null = auto-discover)
CjkFontIndex = 0 // Font index within a .ttc collection
};
TextFittingMode
| Mode | Behavior |
|---|---|
None |
No fitting. Text renders at natural size. |
PreserveWidth |
Compress horizontally to match original text width exactly. |
FitToPage |
Compress only enough to prevent overflow past the page edge. |
Adaptive |
Progressive: character spacing, word spacing, horizontal scaling, font size. Best quality. |
Under Adaptive, MinHorizontalScale and MaxFontSizeReduction are preferred limits: rather
than let a replacement be drawn on top of the text that follows it, the engine keeps shrinking past
them (down to 50 % horizontal scale and 60 % of the original font size), and when even that is not
enough it adds a warning to PdfFindReplaceResult.Warnings and reports the leftover width in
PdfReplacementDetail.OverflowPt.
A wider replacement is never compressed while its line can still take a push: the rest of the line
is moved right by as much room as the line has, and only what is left over is condensed. One
consequence is worth knowing when several pairs land on the same line: each Execute sees the line
as the previous one left it, so replacements keep their natural size until the line is genuinely
full and the one that fills it is the one that gets condensed. Earlier replacements are not
re-balanced afterwards — apply the pairs in one call, or expect the last one on a full line to be
the narrow one.
More than one line
A replacement far longer than the text it replaces has somewhere better to go than into 3 pt
lettering: the paper under its line. Wrap decides whether it may.
TextWrapMode.Auto(default) — honour the line breaks the replacement carries, and word-wrap a line that would otherwise have to be condensed pastMinHorizontalScaleinto the room beside it. The extra lines are drawn at the original size, in its font and colour, one under the other, starting at the match's own x.Explicit— honour the breaks, never wrap on its own; a long line is fitted as before.Off— every break becomes a single space and nothing wraps.
Write a break as a real newline, or as the two characters \ and n — the form that survives a
CSV cell and a single-line text box. \n is a literal backslash-n; two breaks in a row leave
one blank line. A regex replacement is exempt: its is the caller's own business.
PdfFindReplace.Execute("in.pdf", "out.pdf",
"One Example Street, Springfield",
"Postal address: PO Box 42\nStreet address: One Example Street, Springfield",
new PdfFindReplaceOptions { TextFitting = TextFittingMode.Adaptive });
A block is fitted as one piece. Every line of it is condensed by horizontal scale alone, never by letter or word spacing, so its lines are squeezed the same way rather than one being welded together while the next reads cleanly; and all its lines are drawn in one face — the embedded fallback for the whole block as soon as any single character of it is missing from the document's font. A single-line replacement keeps the progressive Tc → Tw → Tz → size fitting described above.
Automatic wrapping needs the match to end its line — a phrase with other words after it is fitted in place, since its second line would run under its own neighbours — and a column worth wrapping into (at least eight font sizes wide). Breaks the caller wrote are honoured wherever the match sits.
The room beside a line now stops one word space before the first picture or painted shape standing
on it, not at the page margin, so a replacement never runs under a logo. Every fit and every tail
push respects that limit, whatever Wrap is set to.
How far down the block may go is the free paper below: whatever is painted or placed under the line
and overlaps it horizontally stops it, as does the page's bottom margin — except a filled panel the
text sits on, which is its background, not its floor (its own bottom edge is the floor instead).
MaxLines caps the count; LineSpacing overrides the pitch, which is otherwise measured from the
page's neighbouring lines when they look like body text (1.0–1.6 × the size) and is 1.2 otherwise.
When the block needs more paper than there is, TextFitting decides who gives way:
Adaptive— the page gives way. Everything below the block's first line moves down by the height of the lines that did not fit: paths, images and text objects alike, and a panel the block sits on is stretched, its top edge staying put while its bottom follows the content. Only the rows down to the first empty band tall enough to take the push move; a footer pinned to the bottom margin stays. A text object whose runs go on past that band is cut there — the pen is nudged down before the rows that move and back up before the first that stays — so a page written as one text object, footer included, still gives way.PdfReplacementDetail.VerticalShiftreports how far andItemsMovedhow much. Something that straddles the line beside the block — a logo next to an address, the second line of a right-aligned title — is left where it is, and text standing beside the line (a logo set in type, a heading in the next column: another baseline, or half as large again) limits the room the lines may use exactly as a picture does. A full-page picture the line is drawn on (a "print to PDF" page whose rules and boxes were rasterised under real text) is read: whatever it has painted under the line is an obstacle like any rule, and it moves with the rows — the picture is drawn in clipped pieces, the piece under the line shifted down. The clipping boxes a Word header is drawn inside are widened to hold the block. The push is refused, with a warning naming the reason, when it would carry content past the page's bottom margin, when a shape or a text object straddles the line over the block, when a picture with something painted under the line cannot be drawn in parts, or when the content below lives in a shared form.FitToPage/PreserveWidth— nothing on the page moves. The block is folded back instead: its last break is undone, then the next, until what is left fits the room below, and the merged line is condensed by the ordinary fitting. A warning says what happened — and a second one, withOverflowPt, when even the smallest size cannot hold the whole replacement on one line.None— the explicit lines are drawn at full size and may overlap: the escape hatch.
PdfReplacementDetail.Lines and LineSpacingUsed report what was drawn. Matches inside a Form
XObject and turned pages (/Rotate) keep the single-line behaviour.
PdfMerger
byte[] Merge(string[] inputPaths);
byte[] Merge(Stream[] inputStreams);
byte[] Merge(byte[][] inputData);
byte[] Merge(IEnumerable<PdfMergeInput> inputs); // With page range selection
void MergeToFile(string[] inputPaths, string outputPath);
PdfSplitter
List<byte[]> Split(string inputPath); // One PDF per page
List<byte[]> Split(Stream inputStream);
List<byte[]> Split(byte[] inputData);
byte[] ExtractPages(string inputPath, int[] pageNumbers); // Selected pages in one PDF
byte[] ExtractPages(Stream inputStream, int[] pageNumbers);
byte[] ExtractPages(byte[] inputData, int[] pageNumbers);
void SplitToFiles(string inputPath, string outputPattern); // Pattern: "page_{0}.pdf"
// Bookmark / table-of-contents based splitting (string, Stream, byte[] overloads)
List<PdfBookmark> GetBookmarks(string inputPath); // Read the outline
List<PdfBookmarkSegment> SplitByBookmarks(string inputPath, // One segment per bookmark
PdfSplitByBookmarksOptions? options = null);
List<string> SplitByBookmarksToFiles(string inputPath, // Write segments to a folder
string outputDir, PdfSplitByBookmarksOptions? options = null);
// async: SplitAsync, ExtractPagesAsync, SplitToFilesAsync,
// GetBookmarksAsync, SplitByBookmarksAsync, SplitByBookmarksToFilesAsync
PdfSplitByBookmarksOptions: MaxDepth (deepest bookmark level used as a split boundary, default 1 = top-level only), FileNamePrefix, FileNameSuffix, IncludeTitleInFileName (default true; false → section_01, section_02, …).
PdfBookmark: Title, Level (1-based), StartPage (1-based, 0 if unresolved). PdfBookmarkSegment: Title, StartPage, EndPage (1-based inclusive), FileName (sanitized + de-duplicated), Data (standalone PDF bytes).
PdfBuilder
PdfBuilder.Create()
.WithMetadata(m => m
.Title("...").Author("...").Subject("...").Creator("...").Keywords("..."))
.AddPage(page => page
.Size(PdfPageSize.A4) // A4, Letter, Legal, A3, A5, Tabloid
.Size(widthPoints, heightPoints) // Custom size (72pt = 1 inch)
.AddText(text, x, y, fontSize, options?) // Positioned text
.AddImage(imageBytes, x, y, width, height) // JPEG or PNG (auto-detected)
.AddLine(x1, y1, x2, y2, strokeWidth?, r?, g?, b?)
.AddRectangle(x, y, w, h, fill?, strokeWidth?,
strokeRed?, strokeGreen?, strokeBlue?,
fillRed?, fillGreen?, fillBlue?)
.AddQrCode(content, x, y, sizePoints?, configure?)) // QrAppearance: ECC, colors, caption, Code 128
.Build(); // Returns byte[]
.BuildToFile(path); // Write to file
.BuildToStream(stream); // Write to stream
Built-in fonts (no embedding needed): Helvetica, Times-Roman, Courier — each with Bold, Italic, BoldItalic variants.
Text options:
.AddText("text", 72, 700, 14, o => o
.Font("Helvetica") // Font family
.Bold() // Bold variant
.Italic() // Italic variant
.Color(1, 0, 0)) // RGB color (0.0 to 1.0)
PdfFormFiller
// Read form fields
List<PdfFormField> GetFields(string path);
List<PdfFormField> GetFields(byte[] pdfData);
List<PdfFormField> GetFields(Stream stream);
// Fill form fields (each method has an overload that accepts PdfFormFillOptions)
PdfFormFillResult Fill(string inputPath, string outputPath, Dictionary<string, string> fieldValues);
PdfFormFillResult Fill(string inputPath, string outputPath, Dictionary<string, string> fieldValues, PdfFormFillOptions options);
byte[] Fill(byte[] inputData, Dictionary<string, string> fieldValues);
byte[] Fill(byte[] inputData, Dictionary<string, string> fieldValues, PdfFormFillOptions options);
PdfFormFillResult Fill(Stream input, Stream output, Dictionary<string, string> fieldValues);
PdfFormFillResult Fill(Stream input, Stream output, Dictionary<string, string> fieldValues, PdfFormFillOptions options);
// Options
class PdfFormFillOptions {
PdfFormFieldAlignment TextAlignment; // Auto (honor field /Q), Left, Center, Right
bool SplitDuplicateWidgets; // Per-widget values via name_1, name_2, ... (auto-flatten)
}
// GetFields with optional split flag
List<PdfFormField> GetFields(string path, bool splitDuplicateWidgets);
List<PdfFormField> GetFields(byte[] data, bool splitDuplicateWidgets);
List<PdfFormField> GetFields(Stream stream, bool splitDuplicateWidgets);
// Flatten — merge field appearances into page content, remove interactive fields
void Flatten(string inputPath, string outputPath);
byte[] Flatten(byte[] inputData);
void Flatten(Stream input, Stream output);
PdfSignatureImage
// Discover signature fields (/FT /Sig)
List<PdfSignatureFieldInfo> GetSignatureFields(string path);
List<PdfSignatureFieldInfo> GetSignatureFields(Stream stream);
List<PdfSignatureFieldInfo> GetSignatureFields(byte[] data);
// Place a JPEG/PNG signature image into a signature field (visual only, no certificate)
PdfSignatureImageResult Fill(string inputPath, string outputPath, byte[] imageBytes, PdfSignatureImageOptions? options = null);
PdfSignatureImageResult Fill(Stream input, Stream output, byte[] imageBytes, PdfSignatureImageOptions? options = null);
byte[] Fill(byte[] inputData, byte[] imageBytes, PdfSignatureImageOptions? options = null);
// Options
class PdfSignatureImageOptions {
string? FieldName; // null = first unsigned signature field
PdfSignatureScaleMode ScaleMode; // FitPreserveAspect (default) | Stretch
PdfSignatureHorizontalAlignment HorizontalAlignment; // Center (default) | Left | Right
PdfSignatureVerticalAlignment VerticalAlignment; // Middle (default) | Bottom | Top
double Padding; // inset in points, default 1.0
bool FlattenAfterFill; // bake into page content (removes ALL fields)
}
PdfSignatureFieldInfo: Name, PageNumber (1-based), Rect (points from bottom-left), WidgetCount, IsSigned, IsReadOnly. PNG transparency is preserved via /SMask; already-signed fields are refused.
PdfSignatureImageResult: FieldsFilled (0 or 1), FilledFieldNames, Warnings.
PdfRedactor
PdfRedactionResult Redact(string inputPath, string outputPath, PdfRedaction[] redactions);
byte[] Redact(byte[] inputData, PdfRedaction[] redactions);
byte[] Redact(Stream input, PdfRedaction[] redactions);
PdfRegionEditor
// Erase everything drawn inside a rectangle (page user space, origin bottom-left)
PdfRegionEditResult Erase(string inputPath, string outputPath, int pageNumber, PdfRect region,
PdfRegionEditOptions? options = null);
byte[] Erase(byte[] data, int pageNumber, PdfRect region, PdfRegionEditOptions? options = null);
PdfRegionEditResult Erase(Stream input, Stream output, int pageNumber, PdfRect region,
PdfRegionEditOptions? options = null);
// Erase, then draw a background / image / text into the rectangle
PdfRegionEditResult Replace(string inputPath, string outputPath, int pageNumber, PdfRect region,
PdfRegionReplacement replacement, PdfRegionEditOptions? options = null);
byte[] Replace(byte[] data, int pageNumber, PdfRect region,
PdfRegionReplacement replacement, PdfRegionEditOptions? options = null);
PdfRegionEditResult Replace(Stream input, Stream output, int pageNumber, PdfRect region,
PdfRegionReplacement replacement, PdfRegionEditOptions? options = null);
// Many regions, any pages, one incremental update
PdfRegionEditResult Apply(string inputPath, string outputPath, IEnumerable<PdfRegionEdit> edits,
PdfRegionEditOptions? options = null);
byte[] Apply(byte[] data, IEnumerable<PdfRegionEdit> edits, PdfRegionEditOptions? options = null);
PdfRegionEditResult Apply(Stream input, Stream output, IEnumerable<PdfRegionEdit> edits,
PdfRegionEditOptions? options = null);
// Async: EraseAsync, ReplaceAsync, ApplyAsync (file + byte[] overloads, CancellationToken)
PdfRegionEdit: PageNumber (1-based), Region (PdfRect), Replacement (null = erase only).
PdfRegionReplacement: ImageBytes (JPEG/PNG, transparency preserved), ImageScaleMode
(ScaleToFit = aspect preserved and centred, default | MatchOriginalSize = stretched to the
region | KeepReplacementSize = natural size, 1 px = 1 pt, centred), Text (PdfRegionText),
BackgroundColor (null = leave transparent).
PdfRegionText: Text (newlines start a new line), FontSize (0 = auto-fit to the region),
FontFamily (mapped to the nearest Standard-14 face: Helvetica / Times / Courier — no
embedding, so WinAnsi characters only), Bold, Italic, Color, HorizontalAlign
(Left | Center | Right), VerticalAlign (Top | Middle | Bottom), Padding (points, default 1).
PdfRegionEditOptions: Match (Inside, default | Intersects), Tolerance (points, default
0.5), EraseText/EraseImages/ErasePaths (all default true), MaxItemAreaFraction (default
0.5 — skips page backgrounds in Intersects mode), SharedForms (Clone, default | EditInPlace |
Skip).
PdfRegionEditResult: ItemsErased, PathsErased, TextItemsErased, ImagesErased,
ImagePlaced, TextPlaced, Warnings.
PdfVectorText
Finds and replaces text that exists on a page only as vector outlines. Reading it needs an OCR
engine, supplied through IOcrTextRecognizer — Exis.PdfOcr.OcrProviderTextRecognizer adapts
any IOcrProvider (Tesseract, Apple Vision).
// Find every recognized word
IReadOnlyList<PdfVectorTextHit> Find(string path, IOcrTextRecognizer ocr,
PdfVectorTextOptions? options = null);
IReadOnlyList<PdfVectorTextHit> Find(byte[] data, IOcrTextRecognizer ocr,
PdfVectorTextOptions? options = null);
IReadOnlyList<PdfVectorTextHit> Find(Stream input, IOcrTextRecognizer ocr,
PdfVectorTextOptions? options = null);
// Find a phrase (spans adjacent words on a line; case and whole-word per the options)
IReadOnlyList<PdfVectorTextHit> Find(string path, string find, IOcrTextRecognizer ocr,
PdfVectorTextOptions? options = null);
IReadOnlyList<PdfVectorTextHit> Find(byte[] data, string find, IOcrTextRecognizer ocr,
PdfVectorTextOptions? options = null);
IReadOnlyList<PdfVectorTextHit> Find(Stream input, string find, IOcrTextRecognizer ocr,
PdfVectorTextOptions? options = null);
// Find several phrases in ONE recognition pass; each hit's MatchedFind names its term
IReadOnlyList<PdfVectorTextHit> Find(string path, IEnumerable<string> finds,
IOcrTextRecognizer ocr, PdfVectorTextOptions? options = null);
IReadOnlyList<PdfVectorTextHit> Find(byte[] data, IEnumerable<string> finds,
IOcrTextRecognizer ocr, PdfVectorTextOptions? options = null);
IReadOnlyList<PdfVectorTextHit> Find(Stream input, IEnumerable<string> finds,
IOcrTextRecognizer ocr, PdfVectorTextOptions? options = null);
// Replace — erases the original outlines and redraws on the same baseline
PdfVectorTextResult Replace(string inputPath, string outputPath, string find, string replace,
IOcrTextRecognizer ocr, PdfVectorTextReplaceOptions? options = null);
PdfVectorTextResult Replace(string inputPath, string outputPath,
IEnumerable<(string find, string replace)> pairs, IOcrTextRecognizer ocr,
PdfVectorTextReplaceOptions? options = null);
PdfVectorTextResult Replace(Stream input, Stream output, string find, string replace,
IOcrTextRecognizer ocr, PdfVectorTextReplaceOptions? options = null);
PdfVectorTextResult Replace(Stream input, Stream output,
IEnumerable<(string find, string replace)> pairs, IOcrTextRecognizer ocr,
PdfVectorTextReplaceOptions? options = null);
byte[] Replace(byte[] data, string find, string replace, IOcrTextRecognizer ocr,
PdfVectorTextReplaceOptions? options = null);
byte[] Replace(byte[] data, IEnumerable<(string find, string replace)> pairs,
IOcrTextRecognizer ocr, PdfVectorTextReplaceOptions? options = null);
// …and the same two with `out PdfVectorTextResult result` when the byte[] overload
// should also report what it did.
// Async: FindAsync (file + byte[], single term and IEnumerable<string>), ReplaceAsync
// (file + byte[] overloads). All take a CancellationToken.
IOcrTextRecognizer: one method —
IReadOnlyList<OcrTextWord> Recognize(byte[] png, int widthPx, int heightPx, string? languageTag).
The PNG is 8-bit grayscale, black ink on white. OcrTextWord: Text, Confidence (0..1),
X/Y/Width/Height (pixels, top-left origin), LineIndex.
PdfVectorTextHit: PageNumber, Text (the match as the page spells it), MatchedFind (the
search term that produced it — empty for a Find that listed every word), Bounds (page user
space, from the vector geometry — exact, not the OCR box), Color, EstimatedFontSize
(cap-height based), LikelyBold, Confidence, ClusterId, LineIndex.
PdfVectorTextOptions: Pages (null = all), RenderScale (pixels per point, default 4.0 ≈
288 dpi), LanguageTag, MinConfidence (default 0.5), CaseSensitive (default false),
WholeWord (default true — off also matches inside a word, and a replacement then redraws the
untouched letters around the new text, since the whole word's outlines have to be erased either
way; either setting tolerates punctuation and spacing differences between the phrase and the OCR
engine's reading of the page, so "Loop , Bradenton" and "Loop, Bradenton" find the line
whether the engine read the comma as its own word or glued it to "Loop"), MinGlyphPixels (default 24 — the smallest height, in pixels, a cluster's typical glyph
is rendered at before OCR; RenderScale is raised per cluster to reach it, so a 4-pt address
line is read at the same quality as body text; 0 renders everything at RenderScale),
ClusterLines (default true — a whole
line reaches the recognizer as one image), MaxClustersPerPage (default 2000), MinClusterHeight
(points, default 3), Progress (IProgress<PdfVectorTextProgress>, reported once per page),
SkipArtworkClusters (default true — do not recognize clusters that are clearly drawings),
ColumnGapFactor (default 4.0 — how far apart, in font sizes, two pieces of a page may sit and
still count as one line; it separates columns for both LineIndex and the tail shift below).
PdfVectorTextProgress: PageNumber, PageCount (of the pages this run covers, not the
document), ClustersOnPage, ClustersSkipped (clusters SkipArtworkClusters passed over —
the difference between "no outlined text here" and "the artwork filter decided this was a
picture"), HitsSoFar.
ICancellableOcrTextRecognizer (extends IOcrTextRecognizer): adds a Recognize overload
taking a CancellationToken, plus a settable CancellationToken property for callers driving the
recognizer directly. Find/Replace only ever call the overload, passing the run's token as an
argument and never touching the property — so a recognizer blocking on a real engine can be
interrupted mid-raster, and one instance can serve two concurrent runs. Exis.PdfOcr's
OcrProviderTextRecognizer implements it.
PdfVectorTextReplaceOptions (extends the above): Mode (Outlines, default — the page stays
all-vector | EmbeddedFont — real text with an embedded TrueType subset), MatchFont (default
true), FontFamily (force a family instead of matching), Bold (bool?, null = use the detected
weight), FontMatchOptions (null = defaults; EmbeddedOnly = true restricts the answer to the
faces this library ships), Color (null = keep the original's colour), MinHorizontalScale
(default 0.8), Fit (ShiftFollowingText, new default in 4.5 — full size, move the rest of
the line |
ShrinkToFit — squeeze into the room before the next word, column, picture or the page's right
limit | PreserveWidth — squeeze into the original's own width | None), PullTailLeftWhenShorter
(default true), MaxShift (points, 0 = unlimited), ShrinkToFit (obsolete, use Fit), Align
(Auto, default | Left | Center | Right), Wrap (Auto, new in 4.6 — honour the
breaks in the replacement and word-wrap a line that would have to be squeezed past
MinHorizontalScale | Explicit — breaks only | Off — a break is a space, nothing wraps),
MaxLines (0 = as many as the paper below allows; 1 = the single fitted line of 4.5),
LineSpacing (multiple of the font size; 0 = measured from the neighbouring lines, else 1.2).
PdfVectorTextResult: Replacements, Details, Warnings.
PdfVectorTextReplacement: PageNumber, Find, Replace, Bounds, FontFamily,
FontStyle, FontScore, Mode, Shift (points the rest of the line moved), Fitted,
Lines (1 unless the replacement wrapped), LineSpacingUsed (points between those baselines).
PdfVectorGraphics
Lists the logos, marks and drawings that exist on a page only as vector paths — the image list, for artwork that has no image XObject — and replaces them with a JPEG or PNG.
// Find
IReadOnlyList<PdfVectorGraphicInfo> Find(string path, PdfVectorGraphicsFindOptions? options = null);
IReadOnlyList<PdfVectorGraphicInfo> Find(byte[] data, PdfVectorGraphicsFindOptions? options = null);
IReadOnlyList<PdfVectorGraphicInfo> Find(Stream input, PdfVectorGraphicsFindOptions? options = null);
// Replace — indices come from a Find on the SAME document, with the same options
PdfVectorGraphicsReplaceResult Replace(string inputPath, string outputPath,
IEnumerable<PdfVectorGraphicReplacement> replacements,
PdfVectorGraphicReplaceOptions? options = null);
PdfVectorGraphicsReplaceResult Replace(Stream input, Stream output,
IEnumerable<PdfVectorGraphicReplacement> replacements,
PdfVectorGraphicReplaceOptions? options = null);
byte[] Replace(byte[] data, IEnumerable<PdfVectorGraphicReplacement> replacements,
PdfVectorGraphicReplaceOptions? options = null);
byte[] Replace(byte[] data, IEnumerable<PdfVectorGraphicReplacement> replacements,
PdfVectorGraphicReplaceOptions? options, out PdfVectorGraphicsReplaceResult result);
// One image for several graphics (graphicIndices null = every graphic)
PdfVectorGraphicsReplaceResult ReplaceAll(string inputPath, string outputPath,
byte[] replacementImage, int[]? graphicIndices = null,
PdfVectorGraphicReplaceOptions? options = null);
PdfVectorGraphicsReplaceResult ReplaceAll(Stream input, Stream output,
byte[] replacementImage, int[]? graphicIndices = null,
PdfVectorGraphicReplaceOptions? options = null);
byte[] ReplaceAll(byte[] data, byte[] replacementImage, int[]? graphicIndices = null,
PdfVectorGraphicReplaceOptions? options = null);
byte[] ReplaceAll(byte[] data, byte[] replacementImage, int[]? graphicIndices,
PdfVectorGraphicReplaceOptions? options, out PdfVectorGraphicsReplaceResult result);
// Async: FindAsync, ReplaceAsync, ReplaceAllAsync (file + byte[] overloads, CancellationToken).
// The token is observed between pages, between the clusters of a page and per thumbnail — not
// only around the call — and a cancelled file overload writes no output.
PdfVectorGraphicInfo: Index (0-based, stable within one Find), PageNumbers (one entry per
occurrence — a graphic drawn twice on a page lists it twice), Bounds (first occurrence, page
user space), OccurrenceBounds (parallel to PageNumbers), PathCount (painting operations, not
shapes), DominantColors (up to four PdfColor, area-weighted, most-used first), ThumbnailPng
(colour PNG on white), WidthPt, HeightPt, Kind, Signature.
VectorGraphicKind: Logo (a compact mark in a handful of colours) | Drawing | TextLike
(type converted to outlines — excluded unless asked for) | Rule (a hairline, never returned) |
Unknown (the colours could not be resolved — a pattern or an unusual colour space).
PdfVectorGraphicsFindOptions: Pages (null = all), MinSizePt (both dimensions, default 12),
MaxPageFraction (default 0.35 — an operation or a cluster covering more is a background or a
frame; 0 or less means no cap), PanelAreaFraction (default 0.04 — a simple filled shape larger
than this fraction of the page, or a rectangle that exactly backs a placed image, is set aside
before clustering so a page-wide band or photo frame cannot chain every mark on the page into
one cluster; 0 or less keeps only the image-backing rule), IncludeTextLike (default false), GapFactor (how close two
paths must be to belong to the same graphic, as a multiple of the smaller of their two heights,
default 0.6 — lower splits a mark from the wordmark beside it; nothing about the rest of the page
enters it, so a graphic's Signature survives an edit elsewhere), DeduplicateAcrossPages
(default true), ThumbnailMaxPx (long side, default 256), MaxGraphics (safety valve, default
500 — it caps how many distinct graphics are listed, not which pages they are found on).
PdfVectorGraphicReplacement: GraphicIndex, ImageBytes (JPEG or PNG), ScaleMode
(ImageScaleMode?, null = the options' mode). Constructor: (int graphicIndex, byte[] imageBytes).
PdfVectorGraphicReplaceOptions: ScaleMode (default ScaleToFit), EraseWholeCluster
(default true; false erases only the paths the placed image covers), BackgroundColor (null =
transparent), SharedForms (Clone, default | EditInPlace | Skip — for artwork inside a Form
XObject that several pages draw), FindOptions (the options the indices came from; null = the
defaults).
PdfVectorGraphicsReplaceResult: GraphicsReplaced, OccurrencesReplaced, ItemsErased,
GraphicsFound, Warnings.
PdfFontMatcher
Identifies the typeface of text that was converted to outlines, by comparing the drawn glyph shapes against the fallback faces embedded in this library plus every TrueType font installed on the machine.
// Rank fonts for a hit from PdfVectorText (the overload replacement uses)
IReadOnlyList<PdfFontMatch> Match(PdfVectorTextHit hit, PdfFontMatchOptions? options = null);
// Rank fonts for the outlines inside a rectangle, when you already know what it says
IReadOnlyList<PdfFontMatch> Match(string inputPath, int pageNumber, PdfRect region, string text,
bool? likelyBold = null, PdfFontMatchOptions? options = null);
IReadOnlyList<PdfFontMatch> Match(byte[] data, int pageNumber, PdfRect region, string text,
bool? likelyBold = null, PdfFontMatchOptions? options = null);
PdfFontMatch: Family, Style (Regular | Bold | Italic | BoldItalic), FullName, IsBold,
IsItalic, IsEmbeddedFallback (true for the Arimo/Tinos/Cousine faces this library ships),
Path (null for those), FontIndex (face inside a .ttc), Score (0..1).
PdfFontMatchOptions: EmbeddedOnly (default false), MaxCandidates (default 400),
MaxGlyphsCompared (default 6), RasterSize (pixels per glyph, default 48), MaxResults
(default 10).
Scores above roughly 0.85 mean the same design; 0.6–0.85 a close relative (a metric-compatible
clone, or another weight of the same family); below 0.5 a different design. Only TrueType-outline
faces are candidates — CFF/PostScript fonts have no glyf table to compare.
PdfImageEditor
// Find images
PdfImageFinderResult FindImages(string inputPath);
PdfImageFinderResult FindImages(Stream input);
// Replace all images
PdfImageReplaceResult ReplaceAll(string inputPath, string outputPath, byte[] replacementImage);
PdfImageReplaceResult ReplaceAll(Stream input, Stream output, byte[] replacementImage);
// Replace with options (filter by page range or image index)
PdfImageReplaceResult Replace(string inputPath, string outputPath, byte[] replacementImage,
PdfImageReplaceOptions? options = null);
PdfImageReplaceResult Replace(Stream input, Stream output, byte[] replacementImage,
PdfImageReplaceOptions? options = null);
PdfImageReplaceOptions: PageRange (1-based page numbers, null = all), ImageIndices (0-based image indices from FindImages, null = all), ScaleMode (MatchOriginalSize | KeepReplacementSize | ScaleToFit, default: MatchOriginalSize).
PdfImageInfo: Index, PageNumbers, PixelWidth, PixelHeight, ColorSpace (RGB/Gray/CMYK/Indexed/Unknown), Format (JPEG/Flate/Raw/JPEG2000), BitsPerComponent, Data (displayable image bytes — JPEG for JPEG images, BMP for Flate/Raw/CMYK/Grayscale; load directly into WPF BitmapImage via MemoryStream), IsMask (true when another image in the result uses this one as its /SMask or /Mask — it only shapes that image's transparency, so lists meant for people should hide it and replacing it changes nothing a viewer shows), MaskOfIndex (the Index of the image it masks; null when it is not a mask). Masks stay in the list and in the counts, so existing callers see what they always did.
PdfImageFinderResult: Images, TotalImages, PagesSearched.
PdfWatermark
PdfWatermarkResult AddText(string inputPath, string outputPath, string text, PdfWatermarkOptions? options = null);
PdfWatermarkResult AddText(Stream input, Stream output, string text, PdfWatermarkOptions? options = null);
byte[] AddText(byte[] inputData, string text, PdfWatermarkOptions? options = null);
PdfWatermarkOptions: Position (Top/Bottom/Center/Across, default: Across), FontSize (default: 48), TextColor (PdfColor, default: light gray), Opacity (0.0–1.0, default: 0.3), PageRange (1-based page numbers, null = all).
PdfBatesStamp
BatesStampResult ApplyBatesStamp(string inputPath, string outputPath, BatesStampOptions? options = null);
BatesStampResult ApplyBatesStamp(Stream input, Stream output, BatesStampOptions? options = null);
byte[] ApplyBatesStamp(byte[] inputData, BatesStampOptions? options = null);
BatesStampOptions:
Prefix/Suffix— text bracketing the number (default: empty).StartNumber(default 1) — first Bates number. For batch continuation, pass the previous document'sLastNumber + 1.Digits(default 6) — minimum zero-padded width. Auto-expands for every page if the range requires more digits, with a warning in the result.Position(defaultBottomRight) — one ofTopLeft,TopCenter,TopRight,BottomLeft,BottomCenter,BottomRight. Corners are relative to the visual page (after/Rotate).FontSize(default 10) — point size. The font is Helvetica (standard 14, no embedding required).TextColor(default black) /BackgroundColor(default none) —PdfColorvalues; background draws a solid rectangle behind the stamp for legibility.MarginInches(default 0.5) — distance from the trimmed page edge. Applied per-page so mixed page sizes (Letter, A4, Legal) share the same visual margin.ConfidentialityLabel— optional adjacent label (e.g."CONFIDENTIAL").ConfidentialityPosition— corner/edge for the label. When null, the label is stacked in the same corner as the Bates number (above for bottom positions, below for top positions) so it never spills past the page edge.ConfidentialityFontSize— label point size. When null, matchesFontSize.PageRange— 1-based page numbers to stamp. Null = all pages.SkipFirstPage(default false) — whenPageRangeis null, skip page 1 (for cover sheets).PageRangewins if both are set.CounterAdvancesOnSkippedPages(default true) — when true, the Bates counter advances for every page whether stamped or not (legal-production convention). When false, only stamped pages consume a number.AllowSignedInput(default false) — opt in to stamp a PDF carrying digital signatures. Stamping invalidates the signatures; a warning is added to the result.WriteXmpMetadata(default true) — write an XMP audit block (range, digit width, prefix/suffix) to the document catalog. Existing XMP metadata is merged, not replaced.QrCode(default null) — aQrStampOptionsfor a QR code (or Code 128) stamped in the same corner on every Bates-stamped page; the number moves beside it. Content defaults to{Bates}; position, margin, pages and Bates numbering come from the Bates options.
Placement uses the visible page: the CropBox (clipped to the MediaBox) as displayed after /Rotate.
BatesStampResult: FirstNumber, LastNumber, PagesStamped, DigitsUsed (equals Digits unless auto-expanded), Warnings (non-fatal diagnostics: digit expansion, signed-input stamped anyway, etc.).
PdfQrStamp
QrStampResult ApplyQrStamp(string inputPath, string outputPath, QrStampOptions options);
QrStampResult ApplyQrStamp(Stream input, Stream output, QrStampOptions options);
byte[] ApplyQrStamp(byte[] inputData, QrStampOptions options);
byte[] ApplyQrStamp(byte[] inputData, QrStampOptions options, out QrStampResult result);
QrStampResult ApplyQrPlaceholders(string inputPath, string outputPath, QrPlaceholderOptions options);
QrStampResult ApplyQrPlaceholders(Stream input, Stream output, QrPlaceholderOptions options);
byte[] ApplyQrPlaceholders(byte[] inputData, QrPlaceholderOptions options, out QrStampResult result);
QrValidationResult Validate(string content, QrAppearance? appearance = null); // no license required
// + ApplyQrStampAsync / ApplyQrPlaceholdersAsync (CancellationToken checked between pages)
QrAppearance (shared by both option classes and the builders):
BarcodeType—QrCode(default) orCode128(ASCII only).SizePoints(default 72) — QR: side of the square including the quiet zone; Code 128: total width.BarHeightPoints— Code 128 bar height (default a quarter ofSizePoints, min 18 pt).QuietZoneModules— default 4 (QR) / 10 (Code 128); smaller values warn.ErrorCorrection—L,M(default),Q,H.ForegroundColor/BackgroundColor— default black on white; foreground must be darker with at leastMinimumContrastRatio(default 4.5, WCAG formula). No opacity.Caption,CaptionFontSize(default 8, shrinks to fit) — text under the code, drawn as Arimo glyph outlines (Latin, Greek, Cyrillic).MinimumModuleSizeMm— default 0.5 (QR) / 0.25 (Code 128);AllowUndersizedturns the error into a warning.
QrStampOptions (positioned stamping) adds: Content (with tokens {FileName}, {FileNameNoExt}, {Page}, {PageCount}, {Bates}, {Date}, {InputSha256}, {AnyValue}), Values, FileName (for stream/byte input), DateFormat, Date, Bates (numbering {Bates} follows), Position (TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter, BottomRight default, Center, Custom with X/Y), MarginInches (default 0.5), Pages (AllPages, FirstPage, LastPage), PageRange, Password, OutputOwnerPassword, AllowSignedInput.
QrPlaceholderOptions adds: Values, RequireValues, FileName, DateFormat, Date, PageRange, Password, OutputOwnerPassword, AllowSignedInput.
QrStampResult: PagesStamped, CodesDrawn, InputSha256, Warnings, Codes (per code: PageNumber, Content, PlaceholderName, Version, ModuleSizeMm, Bounds in visual page points).
QrValidationResult: IsValid, Fits, Version, ModuleCount, ModuleSizeMm, MinimumSizePoints, Message, Warning.
PdfOptimizer
PdfOptimizeResult Optimize(string inputPath, string outputPath, PdfOptimizeOptions? options = null);
PdfOptimizeResult Optimize(Stream input, Stream output, PdfOptimizeOptions? options = null);
byte[] Optimize(byte[] inputData, PdfOptimizeOptions? options = null);
byte[] Optimize(byte[] inputData, PdfOptimizeOptions? options, out PdfOptimizeResult result);
PdfOptimizeOptions: CompressStreams (default true), RemoveDuplicateObjects (default true), RemoveMetadata (default false; removes /Info and the document XMP stream from the file), DownsampleImages (default false), MaxImageDpi (default 150; effective dpi at the drawn size), JpegQuality (default 75), UseObjectStreams (default true). Image downsampling applies to 8-bit raw, losslessly filtered or JPEG (DCTDecode, baseline/progressive Huffman) images (Device*, ICCBased, Cal*, Lab, Separation, DeviceN), including soft masks and images inside form XObjects and annotation appearances.
PdfOptimizeResult: OriginalSize, OptimizedSize, BytesSaved, ReductionPercent, StreamsCompressed, DuplicatesRemoved, ImagesDownsampled, UnreferencedObjectsRemoved.
PdfSigner (net8.0+)
// Sign
byte[] Sign(byte[] inputData, PdfSignOptions options);
byte[] Sign(string inputPath, PdfSignOptions options);
void Sign(string inputPath, string outputPath, PdfSignOptions options);
byte[] Sign(Stream input, PdfSignOptions options);
// Verify first signature
PdfSignatureInfo Verify(byte[] pdfData);
PdfSignatureInfo Verify(string path);
PdfSignatureInfo Verify(Stream stream);
// Verify all signatures
List<PdfSignatureInfo> VerifyAll(byte[] pdfData);
List<PdfSignatureInfo> VerifyAll(string path);
List<PdfSignatureInfo> VerifyAll(Stream stream);
PdfSignOptions: Certificate (X509Certificate2, required), Reason, Location, ContactInfo, SignerName (defaults to certificate subject), Visible (default false — invisible signature), PageNumber (1-based, default 1), Rectangle (PdfSignatureRectangle with X/Y/Width/Height in points, defaults to bottom-left corner). When Visible = true, the signature renders as an annotation on the page showing signer name, date, reason, and location.
PdfSignatureInfo includes: IsSigned, IsValid, SignerName, SignDate, Reason, Location, CertificateSubject, CertificateIssuer, CertificateSerialNumber, CertificateNotBefore, CertificateNotAfter, HasTimestamp, TimestampDate.
PdfAConverter
// Validate (no license required)
PdfAValidationResult Validate(string path, PdfALevel level = PdfALevel.PdfA2b);
PdfAValidationResult Validate(byte[] pdfData, PdfALevel level = PdfALevel.PdfA2b);
PdfAValidationResult Validate(Stream stream, PdfALevel level = PdfALevel.PdfA2b);
// Convert
byte[] Convert(byte[] inputData, PdfALevel level = PdfALevel.PdfA2b);
byte[] Convert(string inputPath, PdfALevel level = PdfALevel.PdfA2b);
void Convert(string inputPath, string outputPath, PdfALevel level = PdfALevel.PdfA2b);
byte[] Convert(Stream input, PdfALevel level = PdfALevel.PdfA2b);
PdfALevel: PdfA1b, PdfA2b, PdfA2u, PdfA3b, PdfA3u. The "u" variants additionally require Unicode mappings (ToUnicode CMap) for all fonts.
PdfTextExtractor
// Simple extraction
PdfTextResult ExtractText(string path);
PdfTextResult ExtractText(Stream stream);
PdfTextResult ExtractText(string path, int[] pages);
PdfTextResult ExtractText(Stream stream, int[] pages);
// Structured extraction (with position and font data)
PdfStructuredTextResult ExtractStructured(string path);
PdfStructuredTextResult ExtractStructured(Stream stream);
PdfStructuredTextResult ExtractStructured(string path, int[] pages);
PdfStructuredTextResult ExtractStructured(Stream stream, int[] pages);
PdfInspector (No License Required)
// Lightweight metadata
PdfDocumentInfo Inspect(string path);
PdfDocumentInfo Inspect(Stream stream);
// Fast page count: page tree only, no text parsing (encrypted: root /Pages /Count, else 0)
int GetPageCount(string path);
int GetPageCount(Stream stream);
int GetPageCount(byte[] data);
Task<int> GetPageCountAsync(string path, CancellationToken ct = default);
Task<int> GetPageCountAsync(Stream stream, CancellationToken ct = default);
// Per-page content classification (Digital / Scanned / AlreadyOcrd / Outlined / Empty)
IReadOnlyList<PageContentInfo> AnalyzePages(string pdfPath);
IReadOnlyList<PageContentInfo> AnalyzePages(Stream pdfStream);
DocumentContentKind ClassifyDocument(string pdfPath);
// Full diagnostic structure dump — for bug reports & support workflows
PdfStructureDump DumpStructure(string path);
PdfStructureDump DumpStructure(byte[] data, string? password = null);
PdfStructureDump DumpStructure(Stream stream, string? password = null);
Task<PdfStructureDump> DumpStructureAsync(string path, CancellationToken ct = default);
Inspect returns: Version, PageCount, Title, Author, Producer, Creator, CreationDate, ModificationDate, IsEncrypted, HasFormFields, FormFieldCount, FontsUsed, per-page WidthInPoints/HeightInPoints/CharacterCount.
DumpStructure returns a PdfStructureDump with: file size, PDF version, object/stream counts, encryption details (V/R/key length/permissions/Filter/SubFilter/StmF/StrF/EFF), FilterChains tally, FontSubtypes/FontEncodings tallies, catalog flags (AcroForm/signed/embedded files/portfolio), UnsupportedStreams (up to 50 streams that failed to decode, with object number, filter chain, subtype, length, and error message), and a Notes list. ToString() produces a multi-line human-readable report suitable for pasting into a bug report.
AnalyzePages returns one PageContentInfo per page (PageNumber, Kind, TextCharCount, TextCoverageRatio, ImageCoverageRatio, HasInvisibleTextLayer, GlyphLikePathCount). ClassifyDocument rolls those up to a single DocumentContentKind (Digital / Scanned / AlreadyOcrd / Outlined — every page is outlined / Mixed / Empty). Both are license-free.
PdfTextLayer
// Append an invisible, selectable text layer to one page (1-based), embedding the supplied
// TrueType font as a CIDFontType2 (Identity-H + ToUnicode). Returns the new PDF bytes.
byte[] AddInvisibleTextLayer(byte[] pdf, int pageNumber,
IReadOnlyList<PositionedText> items, byte[] trueTypeFont);
// Several pages in one pass: one parse, one embedded font, one incremental update — for an
// N-page scanned document this replaces N parses of a growing file and N copies of the font.
byte[] AddInvisibleTextLayers(byte[] pdf,
IReadOnlyDictionary<int, IReadOnlyList<PositionedText>> pageItems, byte[] trueTypeFont);
PositionedText: Text (any Unicode the font covers), X, Y (points; Y is the baseline measured from the bottom edge), FontSize, and optional TargetWidth (horizontally scales the run to span a given width). The visible page content is left untouched — only an appended content stream and the embedded font are added.
PdfXmpMetadata
// Read — license-free, safe on encrypted files (XMP only)
PdfMetadataInfo Extract(string path);
PdfMetadataInfo Extract(byte[] data);
PdfMetadataInfo Extract(Stream input);
// Replace XMP packet (null/"" writes an empty packet; use RemoveXmp to drop entirely)
void SetXmp(string inputPath, string outputPath, string xmpXml);
byte[] SetXmp(byte[] data, string xmpXml);
void SetXmp(Stream input, Stream output, string xmpXml);
// Drop Catalog /Metadata entry
void RemoveXmp(string inputPath, string outputPath);
byte[] RemoveXmp(byte[] data);
void RemoveXmp(Stream input, Stream output);
// Replace /Info dict (throws InvalidOperationException if trailer has no /Info ref)
void SetInfo(string inputPath, string outputPath, PdfInfoDict info);
byte[] SetInfo(byte[] data, PdfInfoDict info);
void SetInfo(Stream input, Stream output, PdfInfoDict info);
// Empty the /Info dict (ref is kept, contents cleared)
void RemoveInfo(string inputPath, string outputPath);
byte[] RemoveInfo(byte[] data);
void RemoveInfo(Stream input, Stream output);
// Drop XMP + empty /Info in one incremental update
void RemoveAll(string inputPath, string outputPath);
byte[] RemoveAll(byte[] data);
void RemoveAll(Stream input, Stream output);
// Every method has a <Name>Async counterpart with CancellationToken
PdfMetadataInfo — HasXmp, XmpXml, XmpByteSize, HasInfo, Info (PdfInfoDict).
PdfInfoDict — Title, Author, Subject, Keywords, Creator, Producer, CreationDate (DateTime?), ModificationDate (DateTime?), Custom (Dictionary<string,string> for non-standard /Info entries). Null-valued fields are written as missing, not as empty strings.
Mutation methods throw NotSupportedException on encrypted input — decrypt first with PdfSecurity.Decrypt.
PdfDocumentBuilder (Auto-Layout)
PdfDocumentBuilder.Create()
.PageSize(PdfPageSize.A4) // Or .PageSize(width, height)
.Margins(72) // Or .Margins(top, right, bottom, left)
.WithMetadata(m => m.Title("..."))
.Header(h => h
.AddText("Title", PdfHorizontalAlignment.Center, fontSize, options?)
.AddPageNumber("Page {page} of {pages}", alignment?, fontSize?, options?)
.AddLine(thickness?, r?, g?, b?))
.Footer(f => /* same as header */)
.AddParagraph(text, fontSize?, options?) // Auto-wrapped, auto-paginated
.AddSpacing(points)
.AddHorizontalRule(thickness?, r?, g?, b?)
.AddImage(imageBytes, width, height)
.AddQrCode(content, sizePoints?, alignment?, configure?) // Moves to the next page if it does not fit
.AddTable(t => t
.Columns(2, 1, 1) // Relative widths
.ColumnsFixed(200, 100, 100) // Or fixed widths in points
.BorderWidth(0.5).BorderColor(r, g, b)
.CellPadding(4)
.AlternatingRowBackground(0.95, 0.95, 1.0) // Light blue on even rows
.HeaderRow(r => r.AddCell("...")) // Repeated on page breaks
.AddRow(r => r
.AddCell("...", o => o
.VerticalAlignment(PdfVerticalAlignment.Middle) // Top, Middle, Bottom
.PaddingLeft(8).PaddingRight(8)))) // Per-side padding
.AddPageBreak()
.Build(); // Returns byte[]
.BuildToFile(path);
.BuildToStream(stream);
Async Overloads
Every I/O API has an async counterpart directly on the main class with CancellationToken support:
// Pattern: <ClassName>.<MethodName>Async(...)
byte[] merged = await PdfMerger.MergeAsync(inputPaths, cancellationToken);
PdfTextResult text = await PdfTextExtractor.ExtractTextAsync(stream, cancellationToken);
var info = await PdfInspector.InspectAsync(path, cancellationToken);
var result = await PdfOptimizer.OptimizeAsync(data, options, cancellationToken);
var sigs = await PdfSigner.VerifyAllAsync(path, cancellationToken);
// Also: PdfFindReplace.ExecuteAsync, PdfSplitter.SplitAsync,
// PdfFormFiller.FillAsync/FlattenAsync,
// PdfSignatureImage.FillAsync/GetSignatureFieldsAsync,
// PdfRedactor.RedactAsync,
// PdfImageEditor.FindImagesAsync/ReplaceAsync/ReplaceAllAsync,
// PdfWatermark.AddTextAsync, PdfBatesStamp.ApplyBatesStampAsync,
// PdfQrStamp.ApplyQrStampAsync/ApplyQrPlaceholdersAsync,
// PdfAConverter.ValidateAsync/ConvertAsync,
// PdfPageEditor.RotateAsync/CropAsync/ReorderAsync/DeletePagesAsync,
// PdfStamper.OverlayAsync/UnderlayAsync,
// PdfSecurity.DecryptAsync/EncryptAsync,
// PdfXmpMetadata.ExtractAsync/SetXmpAsync/RemoveXmpAsync/
// SetInfoAsync/RemoveInfoAsync/RemoveAllAsync
Evaluation & Licensing
| Mode | Limit | How to activate |
|---|---|---|
| Trial | Full access for 14 days | ExisLicense.Initialize() (no key) |
| Evaluation | 3-page limit | After trial expires |
| Licensed | Unlimited | ExisLicense.Initialize("XXXX-XXXX-XXXX-XXXX") |
PdfInspector (including Inspect, AnalyzePages/ClassifyDocument, and DumpStructure), PdfAConverter.Validate, PdfSecurity.GetEncryptionInfo, and PdfXmpMetadata.Extract work without any license.
Purchase at pdfbatcheditor.com/developers.
License
Copyright (c) Exis LLC 2024-2026. All rights reserved. Commercial license required for production use. See LICENSE.md for details.
Made in USA.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.HashCode (>= 1.1.1)
- Microsoft.Win32.Registry (>= 4.7.0)
- System.Buffers (>= 4.5.1)
- System.Memory (>= 4.5.5)
- System.Security.Cryptography.ProtectedData (>= 8.0.0)
- System.Text.Json (>= 8.0.5)
-
net8.0
- Microsoft.Win32.Registry (>= 5.0.0)
- System.Security.Cryptography.Pkcs (>= 8.0.1)
- System.Security.Cryptography.ProtectedData (>= 8.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Exis.PdfEditor:
| Package | Downloads |
|---|---|
|
Exis.PdfOcr
Local, no-cloud OCR for PDFs built on Exis.PdfEditor. Turns scanned (image) PDF pages into searchable PDFs by adding an invisible, selectable text layer (embedded Unicode font, Identity-H) over the page image, so batch find/replace works on scans just like on digital PDFs. Already-digital and already-OCR'd pages pass through untouched. Per-page OCR confidence is surfaced so callers can flag low-confidence pages for human review. The rasterizer is pluggable via IPdfRasterizer; the OCR engine via IOcrProvider. This core package is cross-platform and has no native dependencies — add Exis.PdfOcr.Windows for the Windows rasterizer and Exis.PdfOcr.Tesseract for the default OCR engine. Made in USA. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.8.3 | 68 | 10/20/2026 |
| 4.8.2 | 62 | 10/20/2026 |
| 4.8.1 | 208 | 10/19/2026 |
| 4.7.3 | 110 | 10/5/2026 |
| 4.7.2 | 110 | 10/2/2026 |
| 4.7.1 | 95 | 10/1/2026 |
| 4.7.0 | 96 | 9/30/2026 |
| 4.6.9 | 110 | 9/30/2026 |
| 4.6.7 | 102 | 9/29/2026 |
| 4.6.6 | 108 | 9/29/2026 |
| 4.6.4 | 101 | 9/28/2026 |
| 4.6.3 | 110 | 9/28/2026 |
| 4.6.2 | 111 | 9/22/2026 |
| 4.6.1 | 121 | 9/20/2026 |
| 4.6.0 | 111 | 9/18/2026 |
| 4.5.9 | 105 | 9/17/2026 |
| 4.5.8 | 126 | 9/13/2026 |
| 4.5.7 | 116 | 9/12/2026 |
| 4.5.6 | 110 | 9/12/2026 |
| 4.5.5 | 107 | 9/11/2026 |