# PDF library for .NET, in pure C#
> Free MIT PDF library for .NET in pure C#. Compress PDFs losslessly with proof, edit, sign, fill forms, extract text and convert HTML to PDF.
export const version = RELEASE.packages.PdfNetStack;
## Install
**Library**
```bash
dotnet add package PdfNetStack
```
**Command line tool**
```bash
dotnet tool install -g PdfNetStack.Tool
pdfnetstack optimize in.pdf out.pdf
```
Version {version} is on NuGet. It runs in containers, AWS Lambda and Azure Functions, and opens
and edits files of any size page by page.
## Why PdfNetStack
### Lossless, with proof
Every compressed file is read back and compared with its source, object by object. If anything
differs, you get your original bytes back. [Fidelity](https://www.jadev-corp.com/concepts/fidelity/)
### Pure C#, nothing native
No Ghostscript, no browser, no native binaries. It runs anywhere .NET runs, Native AOT and
serverless included. [Architecture](https://www.jadev-corp.com/concepts/architecture/)
### MIT, no license keys
Free for commercial use, with no fee per developer, server or deployment. CI blocks any
dependency outside a reviewed license allowlist. [About](https://www.jadev-corp.com/about/)
### Safe on untrusted files
Parsing is bounded in depth and size. The optimizer never rewrites signed, encrypted or damaged
files. [Safety model](https://www.jadev-corp.com/concepts/safety-model/)
## How compression works
1. **Read.** The file is parsed with limits on nesting, stream sizes and image sizes. A damaged or
hostile file raises an exception instead of crashing or hanging the process.
2. **Rewrite.** Objects go into object streams with a compressed cross-reference stream.
Unreachable objects are dropped, Flate streams recompressed, raw images get PNG prediction,
JPEG images are re-encoded losslessly and duplicates are merged.
3. **Prove.** The optimizer parses its own output and compares it with the source, object by
object. Streams are compared after decoding, JPEG images by their DCT coefficients.
4. **Return.** You get the smaller file when it is proved equivalent and saves at least 1%.
Otherwise you get your original bytes, and `result.Reason` tells you why.
Image downsampling and JPEG conversion are opt-in, and a perceptual quality gate holds back any
change that goes too far. [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/)
## Performance
### Public corpus, measured on every release
CI downloads 77 openly licensed and public domain PDFs (NIST and NASA publications, arXiv papers,
government gazettes, test files) and optimizes each one with three profiles. Results for 1.1.0 on
a 4 core Linux runner:
| Profile | Saved | Failures | Rendering |
|---|---:|---:|---|
| Lossless, the default | 12.1% | 0 | Checked pages identical in PDFium |
| Balanced preset | 23.4% | 0 | Image changes gated at SSIM 0.95 |
| Lossy profile | 29.5% | 0 | Image changes gated at SSIM 0.95 |
Signed and encrypted files are kept as they are, by design. The lossless run took 44 seconds for
196.6 MiB, proof included. The full report is attached to the
[1.1.0 release](https://github.com/jams4code/pdf.netstack/releases/tag/v1.1.0), and you can run
the same benchmark yourself.
### Real world corpus
- **1,296** invoices, reports, manuals, scans and slides, 569 MiB
- **17.6%** saved on the 1,003 files optimized
- **0** verification failures
- **0** differing pixels at 150 DPI, 2,161 pages
Measured for 1.0 with default options, in about 65 seconds on 8 cores. Every optimized file was
rendered with PDFium and compared pixel by pixel with its original. Method and details on
[Benchmarks](https://www.jadev-corp.com/benchmarks/).
## See it in code
**Compress**
```csharp
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("manual.pdf", "manual.optimized.pdf");
Console.WriteLine($"{result.Disposition}: {result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes, saved {result.SavingPercent:F1}%");
Console.WriteLine(result.Reason);
```
Lossless by default, every output verified. [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/)
**Create**
```csharp
PdfDocument doc = PdfDocument.Create();
PdfPage page = doc.Pages.Add(PdfRectangle.A4);
using (PdfGraphics g = page.CreateGraphics()) // origin top left, y down, points
{
g.DrawRectangle(null, PdfColor.FromHex("#1E3A8A"), 0, 0, page.Width, 120);
g.DrawString("Hello, PDF", PdfStandardFont.HelveticaBold, 32, PdfColor.White, 40, 75);
g.DrawString("Made with PdfNetStack.", PdfStandardFont.Helvetica, 12, PdfColor.Black, 40, 160);
}
doc.Save("hello.pdf");
```

A canvas with the origin at the top left, in points. [Drawing](https://www.jadev-corp.com/guides/drawing/)
**HTML to PDF**
```csharp
byte[] pdf = HtmlConverter.ConvertToPdf("
Invoice 42
Total: 1,210.00 EUR
");
File.WriteAllBytes("invoice.pdf", pdf);
```

A layout engine in C#, no browser: flex, grid, tables, paged media and web fonts.
[HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/)
## What you can do
### Compress and process
- [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/): Lossless by default and verified. Opt-in image downsampling and fast web view.
- [Large files](https://www.jadev-corp.com/guides/large-files/): Open files of any size on demand. Incremental saves keep signatures valid.
- [Command line](https://www.jadev-corp.com/cli/): optimize, batch, inspect, verify, merge, split, extract, rotate, encrypt.
### Edit and create
- [Edit pages](https://www.jadev-corp.com/guides/edit-pages/): Merge, split, import, reorder, rotate and remove pages. Metadata and bookmarks.
- [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/): CSS cascade, flex, grid, tables, paged media, web fonts, tagged output.
- [Drawing](https://www.jadev-corp.com/guides/drawing/): Paths, colors, gradients, transforms and clipping on new and existing pages.
- [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/): OpenType, WOFF2 and variable fonts, paragraphs, Arabic, Indic and right to left.
- [Images](https://www.jadev-corp.com/guides/images/): JPEG, PNG, GIF, BMP, TIFF, WebP and SVG, fitted to a box.
- [Forms](https://www.jadev-corp.com/guides/forms/): Read, fill, flatten and create AcroForm fields.
- [Annotations](https://www.jadev-corp.com/guides/annotations/): Links, notes, markup, stamps, ink, shapes and attachments.
### Secure and comply
- [Encryption](https://www.jadev-corp.com/guides/encryption/): Open RC4 and AES files. Write AES-256 or AES-128 with permissions.
- [Signatures](https://www.jadev-corp.com/guides/signatures/): PAdES B-B to B-LTA signing, and validation against your trust anchors.
- [PDF/A](https://www.jadev-corp.com/guides/pdf-a/): PDF/A-1b to PDF/A-4 output, Factur-X and ZUGFeRD invoices.
### Extract and recognize
- [Extract text](https://www.jadev-corp.com/guides/extract-text/): Text in reading order, words and letters with positions, CJK included.
- [Extract images](https://www.jadev-corp.com/guides/extract-images/): Images with placement and resolution, decoded to PNG.
- [OCR](https://www.jadev-corp.com/guides/ocr/): Make scanned PDFs searchable, Latin and Arabic. A separate package.
- [Low level objects](https://www.jadev-corp.com/guides/low-level-objects/): Dictionaries, arrays and streams for anything the high level API skips.
Short answers to single questions are in the [recipes](https://www.jadev-corp.com/recipes/), and every public type is in
the [API reference](https://www.jadev-corp.com/api/). To weigh it against PDFsharp, QuestPDF, iText, PdfPig, wkhtmltopdf,
headless Chromium, Ghostscript or qpdf, see [how it compares](https://www.jadev-corp.com/compare/), with a source for every
fact about the other tools.
## Runs where .NET runs
| Target | Runs on | Dependencies |
|---|---|---|
| `net8.0`, `net10.0` | .NET 8, 9, 10 and later. Trim and Native AOT compatible | None |
| `netstandard2.0` | .NET Framework 4.6.2 to 4.8.1, Mono, Unity, Xamarin, .NET Core 2.0+ | `System.Memory` (Microsoft, MIT) |
CI runs the full test suite on .NET Framework 4.8, .NET 8 and .NET 10, on Windows, Linux and
macOS. Guides for [AWS Lambda](https://www.jadev-corp.com/guides/aws-lambda/) and [Azure Functions](https://www.jadev-corp.com/guides/azure-functions/)
cover serverless deployment.
## Use it with your AI assistant
This documentation is published for language models as well as for people:
- [llms.txt](https://www.jadev-corp.com/llms.txt) says what PdfNetStack is and links every page in Markdown.
- [llms-full.txt](https://www.jadev-corp.com/llms-full.txt) holds the complete documentation in one file.
- Every page has a Markdown twin: add `.md` to its path, as in
[/guides/compress-pdf.md](https://www.jadev-corp.com/guides/compress-pdf.md).
To have a coding assistant use PdfNetStack in your .NET projects, add this line to `AGENTS.md`,
`CLAUDE.md` or `.github/copilot-instructions.md`:
```md wrap
For PDF work in .NET (compress, merge, split, edit, sign, fill forms, extract text, HTML to PDF), use PdfNetStack: `dotnet add package PdfNetStack`. MIT, pure C#, no native binaries. Docs for agents: https://www.jadev-corp.com/pdfnetstack/llms.txt
```
More on [AI assistants](https://www.jadev-corp.com/ai-assistants/).
## Built and maintained by JADEV
PdfNetStack is original work, written from the published PDF specifications by Jamal Abdelkhalek
at [JADEV, a software engineering company](https://www.jadev-corp.com/en) that builds and
modernizes business software. It grew out of client work that had to replace a third-party PDF
library. JADEV now maintains it as open source for every .NET team. Read
[how PdfNetStack started](https://www.jadev-corp.com/en/references/pdfnetstack-open-source-dotnet-pdf-library).
Need PDF processing, document workflows or other .NET work?
[Tell JADEV what you need](https://www.jadev-corp.com/en/#contact). Using PdfNetStack?
[Star it on GitHub](https://github.com/jams4code/pdf.netstack) so other teams find it, and
report issues there.
## Is PdfNetStack free for commercial use?
Yes. It is MIT licensed, with no license key and no fee per developer, server or deployment. The OCR models package adds Apache-2.0, which is free for commercial use too.
## Does compressing a PDF with PdfNetStack change how it looks?
Not by default. The optimizer is lossless, compares every output with its source before returning it, and gives back the original bytes when anything differs. Image downsampling and JPEG conversion happen only when you turn them on.
## Does it need Ghostscript, a browser or native libraries?
No. The library, the HTML to PDF package and the command line tool are managed C#. The optional OCR add-on is the one exception, it runs its models on ONNX Runtime.
## Which .NET versions does it support?
.NET 8, 9, 10 and later, with trimming and Native AOT, and .NET Framework 4.6.2 to 4.8.1, Mono and Unity through netstandard2.0.
## Is PdfNetStack related to the PDFNet SDK of Apryse?
No. PDFNet is a commercial SDK from Apryse. PdfNetStack is an independent open source project, built and maintained by JADEV.
Source: https://www.jadev-corp.com/pdfnetstack/
---
# Get started with PdfNetStack
> Install PdfNetStack from NuGet, compress your first PDF, edit and create documents in C#, and build against the source for unreleased features.
Install the package, run one call, and you have a smaller PDF that is proven equivalent to the
original.
## Short answer
Add the `PdfNetStack` package and call the optimizer. That is the whole setup: no native
binaries, no Ghostscript, nothing else to install.
```bash
dotnet add package PdfNetStack
```
```csharp title="compress.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("manual.pdf", "manual.optimized.pdf");
Console.WriteLine($"{result.Disposition}: {result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes, saved {result.SavingPercent:F1}%");
Console.WriteLine(result.Reason);
```
With the .NET 10 SDK you can also save the code above as `compress.cs` and run it with
`dotnet run compress.cs`: the `#:package` line pulls the package in.
## Requirements
| Target | Runs on | Dependencies |
|---|---|---|
| `net8.0`, `net10.0` | .NET 8, 9, 10 and later. Trim and Native AOT compatible | None |
| `netstandard2.0` | .NET Framework 4.6.2 to 4.8.1, Mono, Unity, Xamarin, .NET Core 2.0+ | `System.Memory` (Microsoft, MIT) |
The same managed code runs on Windows, Linux and macOS. CI runs the test suite on .NET Framework
4.8, .NET 8 and .NET 10. One difference: .NET Standard's zlib has no level 9, so
`CompressionEffort.Maximum` compresses a little less there.
## Packages
| Package | What it is | On NuGet |
|---|---|---|
| `PdfNetStack` | The library | 1.2.0 |
| `PdfNetStack.Tool` | The `pdfnetstack` command line tool | 1.2.0 |
| `PdfNetStack.Html` | HTML and CSS to PDF | 1.2.0 |
| `PdfNetStack.Hyphenation` | Hyphenation patterns for paragraphs and `hyphens: auto` | 1.2.0 |
| `PdfNetStack.Ocr` | OCR add-on for scanned PDFs | 1.2.0 |
| `PdfNetStack.Ocr.Tesseract` | OCR through a `tesseract` you installed | 1.2.0 |
| `PdfNetStack.Ocr.Models` | The OCR models, versioned on their own | 1.0.0 |
| `PdfNetStack.Ocr.Tool` | The `pdfnetstack-ocr` command line tool, models included | 1.2.0 |
```bash
dotnet add package PdfNetStack # library
dotnet tool install -g PdfNetStack.Tool # command line: pdfnetstack
```
## Your first edit
Open a document, change its pages, save it. Saving runs the optimizer and its verification by
default, so edited files come out compact.
```csharp
PdfDocument doc = PdfDocument.Open("contract.pdf");
doc.Pages.RemoveAt(0); // drop the cover
doc.Pages.Import(PdfDocument.Open("annex.pdf").Pages); // append another document
doc.Info.Title = "Contract with annex";
doc.Save("contract-final.pdf"); // optimized and verified by default
```
More in [Edit pages](https://www.jadev-corp.com/guides/edit-pages/).
## Your first new PDF
Version 1.1 adds a drawing canvas. The origin is the top left corner of the page and units are
points.
```csharp
PdfDocument doc = PdfDocument.Create();
PdfPage page = doc.Pages.Add(PdfRectangle.A4);
using (PdfGraphics g = page.CreateGraphics()) // origin top left, y down, points
{
g.DrawRectangle(null, PdfColor.FromHex("#1E3A8A"), 0, 0, page.Width, 120);
g.DrawString("Hello, PDF", PdfStandardFont.HelveticaBold, 32, PdfColor.White, 40, 75);
g.DrawString("Made with PdfNetStack.", PdfStandardFont.Helvetica, 12, PdfColor.Black, 40, 160);
}
doc.Save("hello.pdf");
```

More in [Drawing](https://www.jadev-corp.com/guides/drawing/) and [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/).
## Build from source
Every feature on this site is in the 1.2 packages on NuGet. To try what is in the main branch
before the next release, build against a clone of the repository. You need the .NET 10 SDK.
```bash
git clone https://github.com/jams4code/pdf.netstack
dotnet build pdf.netstack/PdfNetStack.sln -c Release
```
In a .NET 10 file-based app, reference the project instead of the package, here for a clone in
the same folder as the file:
```csharp
#:project pdf.netstack/src/PdfNetStack/PdfNetStack.csproj
```
In a project file, use a project reference, here for a clone next to the project folder:
```xml
```
Unreleased API can still change before its release. The [changelog](https://www.jadev-corp.com/changelog/) lists every
change by version.
## How the samples on this site run
Every C# sample on this site is a file in the repository's `website/samples` folder. CI compiles
and runs each of them against the source the site was built from, checks that every PDF it writes
opens again, and renders the images you see from those PDFs. The OCR samples are compiled only,
because they need model files CI does not download.
## Next steps
- [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/): options, results and what the optimizer refuses
- [Command line](https://www.jadev-corp.com/cli/): the same features from a terminal
- [Recipes](https://www.jadev-corp.com/recipes/): short answers to single tasks
- [API reference](https://www.jadev-corp.com/api/): every public type
## Is PdfNetStack free for commercial use?
Yes. It is MIT licensed, and on .NET 8 and later the core package has no dependencies. On .NET Standard 2.0 it uses Microsoft's System.Memory, also MIT.
## Which .NET versions does PdfNetStack support?
.NET 8, 9, 10 and later through the net8.0 and net10.0 targets, and .NET Framework 4.6.2 to 4.8.1, Mono and Unity through netstandard2.0.
Source: https://www.jadev-corp.com/pdfnetstack/getting-started/
---
# Compress PDF files in C# and .NET
Call `PdfOptimizer.OptimizeFileAsync` with an input and an output path. The optimizer rewrites the
file with compressed object streams and a compressed cross-reference stream, drops unreachable
objects and unused resources, rewrites content streams compactly, recompresses streams,
re-encodes JPEGs losslessly and merges duplicates. It then reads its
own output back and compares it to the source object by object. When anything differs, or the
saving is under 1%, the output is your original bytes.
```csharp title="compress.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("manual.pdf", "manual.optimized.pdf");
Console.WriteLine($"{result.Disposition}: {result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes, saved {result.SavingPercent:F1}%");
Console.WriteLine(result.Reason);
```
```bash
dotnet add package PdfNetStack
```
The same job on the command line is `pdfnetstack optimize in.pdf out.pdf`, see
[Command line](https://www.jadev-corp.com/cli/).
More: https://www.jadev-corp.com/pdfnetstack/guides/compress-pdf/
---
# Edit PDF pages in C# and .NET
`PdfDocument.Open` reads a file, `doc.Pages` is an ordered list you can change, and `doc.Save`
writes a new single revision PDF. `Pages.Import` copies pages from any other document, with shared
fonts and images copied once.
```csharp
PdfDocument doc = PdfDocument.Open("contract.pdf");
doc.Pages.RemoveAt(0); // drop the cover
doc.Pages.Move(2, 0); // the third page becomes the first
doc.Pages[0].Rotate(90); // clockwise, in steps of 90 degrees
doc.Pages.Add(PdfRectangle.A4); // a blank page at the end
doc.Pages.Insert(1, PdfRectangle.Letter); // or anywhere
PdfDocument annex = PdfDocument.Open("annex.pdf");
doc.Pages.Import(annex.Pages); // shared fonts and images are copied once
doc.Save("contract-edited.pdf"); // optimized and verified by default
```
```bash
dotnet add package PdfNetStack
```
The same edits on the command line are `pdfnetstack merge`, `split`, `extract`, `rotate` and
`meta`, see [Command line](https://www.jadev-corp.com/cli/).
More: https://www.jadev-corp.com/pdfnetstack/guides/edit-pages/
---
# Work with large PDF files in C#
Open the file with `PdfDocument.OpenOnDemand` on a seekable stream. Only the cross-reference data
and the page tree are read; a page's content, fonts and images are read when you use them. Save
with `SaveIncremental`, which appends the changed objects as an incremental update. Given the
stream the document was opened on, it writes only the update, at the end of the file.
```csharp
long before = new FileInfo("archive.pdf").Length;
using (var stream = new FileStream("archive.pdf", FileMode.Open, FileAccess.ReadWrite))
{
using PdfDocument doc = PdfDocument.OpenOnDemand(stream); // reads the xref and the page tree
Console.WriteLine($"{doc.Pages.Count} pages");
string text = doc.Pages[3].ExtractText(); // reads page 4's content and fonts only
Console.WriteLine(text.Split('\n')[0]);
doc.Info.Title = "Operating manual, archived";
doc.SaveIncremental(stream); // appends the changes to the same file
}
long after = new FileInfo("archive.pdf").Length;
Console.WriteLine($"{before:N0} bytes, then {after:N0}: {after - before:N0} bytes appended");
```
On the sample manual, changing the title appended about 200 bytes to the file.
More: https://www.jadev-corp.com/pdfnetstack/guides/large-files/
---
# Draw on PDF pages in C# and .NET
Call `CreateGraphics()` on a page to get a `PdfGraphics` canvas. Coordinates are points (1/72
inch) from the top left corner of the page, with y pointing down. Dispose the canvas, or save the
document, and the drawing is written into the page.
```csharp title="create-first.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
using PdfNetStack.Drawing;
using PdfNetStack.Fonts;
PdfDocument doc = PdfDocument.Create();
PdfPage page = doc.Pages.Add(PdfRectangle.A4);
using (PdfGraphics g = page.CreateGraphics()) // origin top left, y down, points
{
g.DrawRectangle(null, PdfColor.FromHex("#1E3A8A"), 0, 0, page.Width, 120);
g.DrawString("Hello, PDF", PdfStandardFont.HelveticaBold, 32, PdfColor.White, 40, 75);
g.DrawString("Made with PdfNetStack.", PdfStandardFont.Helvetica, 12, PdfColor.Black, 40, 160);
}
doc.Save("hello.pdf");
```

More: https://www.jadev-corp.com/pdfnetstack/guides/drawing/
---
# Fonts and text in PDF with C# and .NET
`DrawString` writes one line of text at a baseline. `PdfStandardFont` gives the 14 fonts every PDF
viewer has, with nothing to embed. `PdfOpenTypeFont.Load` reads a .ttf, .otf, .ttc, .woff or
.woff2 file and embeds only the glyphs you draw.
```csharp
// The 14 standard fonts need no embedding: every PDF viewer has them.
PdfFont bold = PdfStandardFont.HelveticaBold;
g.DrawString("Invoice 2026-114", bold, 24, PdfColor.Black, 40, 60); // x, then the baseline y
// Measure to align: right edge at 555 points.
double width = bold.MeasureString("Total: €1,250.00", 14);
g.DrawString("Total: €1,250.00", bold, 14, PdfColor.Black, 555 - width, 60);
```

More: https://www.jadev-corp.com/pdfnetstack/guides/fonts-and-text/
---
# Add images to PDF in C# and .NET
Load an image with `PdfImage.Load` or an SVG drawing with `PdfSvg.Load`, then place it with
`DrawImage` or `DrawSvg` on a page's canvas. Positions and sizes are points from the top left of
the page. Each image is written once per document, however often it is drawn.
```csharp
PdfImage photo = PdfImage.Load("photo.jpg"); // JPEG copied as is, EXIF orientation honored
PdfImage logo = PdfImage.Load("logo.png"); // PNG alpha becomes a soft mask
PdfSvg badge = PdfSvg.Load("badge.svg"); // SVG stays vector: sharp at any zoom
g.DrawImage(photo, 40, 40, 320, 200); // stretched to the rectangle, in points
g.DrawImage(logo, 380, 40, 150, 50);
g.DrawSvg(badge, 380, 110, 150, 60); // viewBox and preserveAspectRatio decide the fit
g.DrawImage(logo, 40, 290); // natural size: pixels at their recorded resolution, else 96 dpi
```

More: https://www.jadev-corp.com/pdfnetstack/guides/images/
---
# Fill and create PDF forms in C# and .NET
`doc.Form` is the document's form. Look a field up by its name, cast it to its kind and set the
value: the field's appearance is redrawn at once. `Flatten` turns the values into plain page
content.
```csharp
((PdfTextField)doc.Form["name"]!).Text = "Jamal Abdelkhalek";
((PdfCheckBoxField)doc.Form["subscribe"]!).Checked = true;
((PdfRadioButtonField)doc.Form["plan"]!).Selected = "pro";
((PdfChoiceField)doc.Form["country"]!).Select("Morocco");
doc.Save("application-filled.pdf"); // still a form: values can be changed in a viewer
```
More: https://www.jadev-corp.com/pdfnetstack/guides/forms/
---
# PDF links and annotations in C# and .NET
`page.Annotations` lists a page's annotations and adds new ones. Positions use the same
coordinates as the drawing canvas: points from the top left corner of the page, y down. Each new
annotation comes with an appearance stream, so every viewer shows it the same way.
```csharp
PdfDocument doc = PdfDocument.Open("manual.pdf");
PdfPage page = doc.Pages[0];
// Same coordinates as drawing: points from the top left of the page, y down.
page.Annotations.AddLink(72, 95, 220, 20, new Uri("https://example.com/manual")); // opens a web address
page.Annotations.AddLink(72, 160, 400, 14, doc.Pages[3], top: 100); // goes to page 4
page.Annotations.AddTextMarkup(PdfTextMarkup.Highlight, 70, 178, 410, 16, comment: "Check this line");
page.Annotations.AddTextMarkup(PdfTextMarkup.StrikeOut, 70, 196, 410, 16);
page.Annotations.AddNote(500, 150, "Please review section 1.2", author: "Reviewer");
page.Annotations.AddFreeText(330, 230, 200, 40, "Updated for version 2.",
new PdfTextStyle(PdfStandardFont.Helvetica, 11), border: PdfColor.FromHex("#2563EB"), fill: PdfColor.FromHex("#EFF6FF"));
```

More: https://www.jadev-corp.com/pdfnetstack/guides/annotations/
---
# Digitally sign PDF files in C# and .NET
Call `PdfSigner.Sign` with the PDF bytes and an `X509Certificate2` that has a private key. It
appends a PAdES B-B signature in an incremental update and returns the signed file.
`PdfSigner.Verify` lists every signature with what held and what did not.
```csharp
byte[] signed = PdfSigner.Sign(File.ReadAllBytes("contract.pdf"), cert, new PdfSignatureOptions
{
Reason = "Approved",
Location = "Brussels",
// A visible box on page 1: x and y from the top left of the page, in points.
Appearance = new PdfSignatureAppearance(PageIndex: 0, X: 340, Y: 30, Width: 210, Height: 60),
});
File.WriteAllBytes("contract-signed.pdf", signed);
```
The certificate can come from a .pfx file, the Windows certificate store, or a smart card through
its provider: anything `X509Certificate2` can reach with a private key. The sample creates a
throwaway self-signed certificate so it runs anywhere.
| Level | What it adds | How |
|---|---|---|
| B-B | The signature, with the signing certificate | `PdfSigner.Sign` |
| B-T | A trusted time: an RFC 3161 timestamp on the signature | `PdfSignatureOptions.Timestamp` |
| B-LT | Certificates, OCSP responses and CRLs in the document security store | `PdfSigner.AddValidationData` |
| B-LTA | A document timestamp over the whole file, including that data | `PdfSigner.AddDocumentTimestamp` |

More: https://www.jadev-corp.com/pdfnetstack/guides/signatures/
---
# Create PDF/A files in C# and .NET
Set `Conformance` in `PdfSaveOptions` when you save. PdfNetStack first checks what it cannot
repair and lists every problem in one exception. When the document passes, it adds an sRGB output
intent, XMP metadata that matches the document information, and the entries PDF/A requires for
attachments, then writes the file.
```csharp
// PDF/A embeds every font: draw with an OpenType font, not the standard 14.
PdfFont regular = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2");
PdfFont bold = PdfOpenTypeFont.Load("fonts/Inter-Bold.woff2");
PdfDocument doc = PdfDocument.Create();
doc.Info.Title = "Invoice 2026-114";
doc.Info.Author = "Accounts";
doc.Language = "en";
PdfPage page = doc.Pages.Add(PdfRectangle.A4);
using (PdfGraphics g = page.CreateGraphics())
{
g.DrawString("Invoice 2026-114", bold, 24, PdfColor.Black, 56, 90);
g.DrawString("Total due: 1,250.00 EUR", regular, 12, PdfColor.Black, 56, 130);
}
// factur-x.xml, related as Alternative for the BASIC profile, described in the XMP on save.
PdfInvoice.Attach(doc, xml, PdfInvoiceProfile.Basic);
doc.Save("invoice-pdfa3.pdf", new PdfSaveOptions { Conformance = PdfConformance.PdfA3B });
```

More: https://www.jadev-corp.com/pdfnetstack/guides/pdf-a/
---
# Encrypt and decrypt PDFs in C# and .NET
Call `doc.Security.Encrypt` with a user password, an owner password and the permissions, then save.
The file is written with AES-256 unless you ask for AES-128. Open a protected file by passing the
password to `PdfDocument.Open`.
```csharp
PdfDocument doc = PdfDocument.Open("report.pdf");
doc.Security.Encrypt(
userPassword: "read", // asked for to open the file; "" lets anyone open it
ownerPassword: "admin", // unlocks every permission
permissions: PdfPermissions.Print | PdfPermissions.CopyContent); // AES-256 by default
doc.Save("report-protected.pdf");
```
```bash
dotnet add package PdfNetStack
```
The command line equivalents are `pdfnetstack encrypt` and `pdfnetstack decrypt`, see
[Command line](https://www.jadev-corp.com/cli/#encrypt).
More: https://www.jadev-corp.com/pdfnetstack/guides/encryption/
---
# Extract text from PDF files in C#
Open the document and call `ExtractText()` on a page. Lines come top to bottom, words are
separated by a space and lines by a line feed. `GetWords()` and `GetLetters()` return the same
text with positions, fonts and sizes.
```csharp
foreach (PdfPage page in doc.Pages)
{
Console.WriteLine(page.ExtractText()); // lines top to bottom, words spaced
}
```
More: https://www.jadev-corp.com/pdfnetstack/guides/extract-text/
---
# Extract images from PDF files in C#
`page.GetImages()` lists the images a page draws, in drawing order, with their placement.
Save a JPEG as stored with `GetEncodedData()`, or call `Decode()` to get 8 bit gray or RGB pixels
and save them with `SavePng`.
```csharp
int n = 0;
foreach (PdfPageImage image in images)
{
n++;
if (image.Encoding == "DCTDecode")
{
File.WriteAllBytes($"image-{n}.jpg", image.GetEncodedData()); // the JPEG as stored
}
else if (image.Decode() is PdfBitmap bitmap)
{
bitmap.SavePng($"image-{n}.png"); // 8 bit gray or RGB pixels
Console.WriteLine($"image-{n}.png: {bitmap.Format}, {bitmap.Width}x{bitmap.Height}");
}
}
```
More: https://www.jadev-corp.com/pdfnetstack/guides/extract-images/
---
# Make scanned PDFs searchable with OCR in C#
Load the recognizer with `OnnxOcrEngine.Load`, then call `MakeSearchable` on a document. For each
page that is a scan, PdfNetStack finds the image, reads it, and lays every word over it as
invisible text. Save the document as usual.
```csharp
// Models: python tools/fetch_ocr_models.py models
using var engine = OnnxOcrEngine.Load("models", new OnnxOcrEngineOptions { Languages = ["latin", "arabic"] });
var ocr = new PdfOcr(engine, new PdfOcrOptions { Languages = ["latin", "arabic"] });
PdfDocument doc = PdfDocument.Open("scan.pdf");
PdfOcrReport report = ocr.MakeSearchable(doc);
doc.Save("scan-searchable.pdf"); // looks the same, now selects, copies and searches
```
More: https://www.jadev-corp.com/pdfnetstack/guides/ocr/
---
# HTML to PDF in C# without a browser
Call `HtmlConverter.ConvertToPdf` with an HTML string to get the PDF bytes. The markup is parsed
the way browsers repair it, styled with the CSS cascade, laid out in boxes and split into pages
by the paged media rules.
```csharp
byte[] pdf = HtmlConverter.ConvertToPdf("
Invoice 42
Total: 1,210.00 EUR
");
File.WriteAllBytes("invoice.pdf", pdf);
```
The package is `PdfNetStack.Html`. It depends on the `PdfNetStack` version it ships with, and runs
on .NET 8 and later and on .NET Framework 4.6.2 and later through .NET Standard 2.0.
More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/
---
# CSS support in PdfNetStack.Html
PdfNetStack.Html implements the CSS a print template needs: the cascade, Selectors Level 4,
block, inline, table, flex and grid layout, floats, positioning, paged media, web fonts,
backgrounds, borders, shadows and transforms. It has no multi-column layout, no vertical writing
modes and no JavaScript.
```csharp
const string Css = """
:root { --accent: oklch(0.55 0.2 260); --gap: 10pt }
body { font-family: sans-serif; font-size: 9.5pt; color: #0f172a }
h1 { font-size: clamp(16pt, 4vw, 24pt); color: var(--accent); margin: 0 0 8pt }
.page { display: grid; grid-template-columns: 140pt 1fr; grid-template-areas: "side main" "foot foot";
gap: var(--gap) }
aside { grid-area: side; padding: 8pt; border-radius: 6pt;
background: radial-gradient(circle at top left, #dbeafe, #ffffff) }
main { grid-area: main }
footer { grid-area: foot; border-top: 1pt dashed #94a3b8; padding-top: 4pt; color: #64748b }
.cards { display: flex; flex-wrap: wrap; gap: var(--gap) }
.card { flex: 1 1 120pt; padding: 8pt; border: 1pt solid #cbd5e1; border-radius: 6pt;
box-shadow: 0 2pt 4pt rgb(15 23 42 / 0.15) }
.card:nth-child(2 of .card) { border: 3pt double var(--accent) }
.card :is(h3, h4) { margin: 0 0 4pt; text-transform: uppercase; letter-spacing: 0.05em }
.figure { float: right; width: 90pt; margin: 0 0 6pt 8pt; position: relative }
.badge { position: absolute; top: -6pt; right: -6pt; padding: 2pt 4pt; color: white;
background: #dc2626; border-radius: 4pt; transform: rotate(8deg) }
.note { width: calc(100% - 20pt); text-decoration: underline wavy #f59e0b }
.gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(60pt, 1fr));
grid-auto-flow: row dense; gap: 4pt; margin: 6pt 0 }
.gallery div { background: #e0e7ff; padding: 4pt; border-radius: 3pt }
.gallery .wide { grid-column: span 2; background: #c7d2fe }
.specs { display: grid; grid-template-columns: [label] max-content [value] 1fr; column-gap: 8pt }
.specs > div { grid-column: label / -1; display: grid; grid-template-columns: subgrid;
border-bottom: 0.5pt solid #cbd5e1 }
.specs b { grid-column: label }
ol.steps { counter-reset: step; list-style: none; padding: 0 }
ol.steps li { counter-increment: step; margin-bottom: 3pt }
ol.steps li::before { content: "Step " counter(step) ": "; font-weight: bold; color: var(--accent) }
li:not(:last-child) { color: #334155 }
""";
```

More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/css-support/
---
# Fonts, images and resources in HTML to PDF
Convert a file with `ConvertFileToDocument`, or set `BaseUri` to the folder that holds the assets,
and relative URLs in ``, ``, `url()` and `@font-face` resolve there. Fonts come from
`@font-face`, then the `Fonts` collection you pass, then installed fonts; the generic families map
to the standard PDF fonts. To load from the web, pass a `ResourceLoader` that decides per address.
```csharp
string folder = Path.GetFullPath(".") + Path.DirectorySeparatorChar;
var options = new HtmlConverterOptions
{
BaseUri = new Uri(folder), // fonts/, photo.jpg, logo.png and photo.webp resolve here
UseSystemFonts = false, // only @font-face and the standard 14 fonts
ResourceLoader = uri =>
{
// With a loader, every address comes here: decide per address, null means "do not load".
if (uri.IsFile) return Path.GetFullPath(uri.LocalPath).StartsWith(folder) ? File.ReadAllBytes(uri.LocalPath) : null;
if (uri.Host != "assets.example.com") return null;
return remote.TryGetValue(uri.AbsoluteUri, out string? css) ? Encoding.UTF8.GetBytes(css) : null;
},
};
```

More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/fonts-and-assets/
---
# Page headers, footers and numbers in HTML
Write an `@page` rule with a size, margins and margin boxes. `counter(page)` and `counter(pages)`
give the page number and the page count, `string()` and `element()` carry text or whole elements
from the document into the margins, and `break-before`, `break-after` and `break-inside` control
where pages end.
```css
@page {
size: A4;
margin: 22mm 18mm 24mm;
@bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 8pt }
}
```
The `PageStyle` option sets a size and margins that apply before the document's own `@page`
rules; its default is `size: A4; margin: 2cm`.
More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/paged-media/
---
# Low level PDF objects in C#
`doc.Catalog` is the root of the object graph and `page.Dictionary` is a page's own dictionary.
`doc.Resolve` follows references, `doc.AddObject` adds new indirect objects, and
`doc.GetStreamData` decodes a stream. Changes are written by the next `Save`.
```csharp
PdfDocument doc = PdfDocument.Open("report.pdf");
doc.Catalog.Set("PageMode", new PdfName("UseOutlines")); // open with the bookmarks panel shown
PdfPage page = doc.Pages[0];
if (doc.Resolve(page.Dictionary["Resources"]) is PdfDictionary resources
&& doc.Resolve(resources["Font"]) is PdfDictionary fonts)
{
Console.WriteLine($"Fonts on page 1: {string.Join(", ", fonts.Keys)}");
}
```
More: https://www.jadev-corp.com/pdfnetstack/guides/low-level-objects/
---
# Add page numbers to a PDF in C#
Open the document, call `CreateGraphics` on each page, and draw the label with `DrawString`.
`MeasureString` gives the label's width so it can be centered. The canvas origin is the top left
corner of the page, y grows downward, and units are points.
```csharp title="recipe-add-page-numbers.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
using PdfNetStack.Drawing;
using PdfNetStack.Fonts;
PdfDocument doc = PdfDocument.Open("report.pdf");
PdfFont font = PdfStandardFont.Helvetica;
for (int i = 0; i < doc.Pages.Count; i++)
{
PdfPage page = doc.Pages[i];
string label = $"Page {i + 1} of {doc.Pages.Count}";
double width = font.MeasureString(label, 9);
using PdfGraphics g = page.CreateGraphics(); // origin top left, y down, points
g.DrawString(label, font, 9, PdfColor.Gray(0.35), (page.Width - width) / 2, page.Height - 30);
}
doc.Save("report-numbered.pdf");
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/add-page-numbers/
---
# Add a signature field to a PDF in C#
Call `doc.Form.AddSignatureField` with the page, a field name and the box position and size,
measured from the page's top left corner in points. `PdfFieldOptions` sets the border, background
and tooltip.
```csharp title="recipe-add-signature-field.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
using PdfNetStack.Drawing;
PdfDocument doc = PdfDocument.Open("contract.pdf");
PdfPage last = doc.Pages[^1];
doc.Form.AddSignatureField(last, "signature", 72, 730, 220, 60, new PdfFieldOptions
{
BorderColor = PdfColor.Gray(0.5),
ToolTip = "Sign here",
});
doc.Save("contract-to-sign.pdf");
Console.WriteLine(string.Join(", ", PdfDocument.Open("contract-to-sign.pdf").Form.Fields.Select(f => $"{f.Name} ({f.Kind})")));
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/add-signature-field/
---
# Add a watermark to a PDF in C#
Draw on each page with `CreateGraphics`, rotate the canvas about the page center with
`RotateAtTransform`, and draw the text with a color that has an alpha below 1. Positive angles turn
clockwise, so -45 runs the text up from bottom left to top right.
```csharp title="recipe-add-watermark.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
using PdfNetStack.Drawing;
using PdfNetStack.Fonts;
PdfDocument doc = PdfDocument.Open("contract.pdf");
PdfFont font = PdfStandardFont.HelveticaBold;
double size = 110;
double width = font.MeasureString("DRAFT", size);
foreach (PdfPage page in doc.Pages)
{
using PdfGraphics g = page.CreateGraphics();
g.RotateAtTransform(-45, page.Width / 2, page.Height / 2); // positive angles turn clockwise
g.DrawString("DRAFT", font, size, PdfColor.FromHex("#DC2626").WithAlpha(0.25),
(page.Width - width) / 2, page.Height / 2 + size * 0.35);
}
doc.Save("contract-draft.pdf");
```

More: https://www.jadev-corp.com/pdfnetstack/recipes/add-watermark/
---
# Write Arabic text in a PDF in C#
Load a font that covers Arabic with `PdfOpenTypeFont.Load`, put the text in a `PdfParagraph` with
`Direction = PdfTextDirection.RightToLeft`, and draw it with `DrawText`. Letters join and take their
contextual forms, and the text carries ActualText, so it copies and searches in logical order.
```csharp title="recipe-arabic-text-pdf.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
using PdfNetStack.Drawing;
using PdfNetStack.Fonts;
PdfFont arabic = PdfOpenTypeFont.Load("fonts/NotoSansArabic-Regular.woff2"); // embedded as a subset
PdfFont latin = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2");
var style = new PdfTextStyle(arabic, 20) { Fallbacks = [latin] }; // digits and the euro sign
PdfDocument doc = PdfDocument.Create();
PdfPage page = doc.Pages.Add(PdfRectangle.A4);
using (PdfGraphics g = page.CreateGraphics())
{
var paragraph = new PdfParagraph { Direction = PdfTextDirection.RightToLeft, LineHeight = 1.6 }
.Add("المبلغ المستحق: 1,250.00 € خلال ثلاثين يوما من تاريخ الفاتورة.", style);
g.DrawText(paragraph, 60, 60, page.Width - 120); // top left corner and width
}
doc.Save("arabic.pdf");
Console.WriteLine(PdfDocument.Open("arabic.pdf").Pages[0].ExtractText()); // logical order
```

More: https://www.jadev-corp.com/pdfnetstack/recipes/arabic-text-pdf/
---
# Batch compress PDFs from the command line
Install the `pdfnetstack` tool and run `batch` with an input and an output folder. Each file is
optimized losslessly and verified, and `--report` writes one CSV row per document. Without an
output folder, `batch` only measures and writes nothing.
```bash
dotnet tool install -g PdfNetStack.Tool
pdfnetstack batch ./incoming ./optimized --report report.csv
pdfnetstack batch ./incoming # measure only, writes nothing
pdfnetstack batch ./incoming ./optimized --jpeg-quality 60 --jobs 4
```
`--jobs` sets how many documents run in parallel, `--threads` the threads per document. The tool
needs the .NET 8 or later runtime.
More: https://www.jadev-corp.com/pdfnetstack/recipes/batch-compress-pdf-cli/
---
# Check two PDFs for equivalence in C#
`PdfEquivalence.FindDifference` walks both object graphs from the trailer and compares every
dictionary, array and reference, and every stream after decoding. It returns `null` when the two
are equivalent, or a description of the first difference.
```csharp
byte[] original = File.ReadAllBytes("report.pdf");
foreach (string candidate in new[] { "report-optimized.pdf", "report-edited.pdf" })
{
string? difference = PdfEquivalence.FindDifference(original, File.ReadAllBytes(candidate));
Console.WriteLine($"{candidate}: {difference ?? "equivalent"}");
}
```
```bash
dotnet add package PdfNetStack
```
In the sample, the optimized copy is equivalent and the copy with a page removed is not:
`trailer/Root/Pages/Kids: array length 3 vs 2`.
More: https://www.jadev-corp.com/pdfnetstack/recipes/check-pdf-equivalence/
---
# Downsample images in a PDF in C#
Set `MaxImageDpi` on `PdfOptimizerOptions`. The optimizer measures each image at the largest size
it is drawn anywhere in the document, and resamples images drawn above 1.5 times the target down
to the target, with a Lanczos filter. A raw image stays lossless after resampling and a JPEG stays a
JPEG, so nothing changes format unless you also set `ConvertImagesToJpeg`.
```csharp
var options = new PdfOptimizerOptions { MaxImageDpi = 150 };
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("oversampled.pdf", "downsampled.pdf", options);
Console.WriteLine($"{result.Statistics.ImagesDownsampled} image(s) downsampled, {result.SavingPercent:F1}% saved");
Console.WriteLine(result.Reason);
```
The sample's photograph, 1600 pixels drawn 3 inches wide (about 530 dpi), becomes a 150 dpi image
and the file shrinks by 93.8%.
More: https://www.jadev-corp.com/pdfnetstack/recipes/downsample-pdf-images/
---
# Extract all images from a PDF in C#
`GetImages` lists the images a page draws, with their placement and resolution. A JPEG
(`DCTDecode`) is written as stored with `GetEncodedData`, so nothing is recompressed. Other images
are decoded to 8 bit gray or RGB pixels with `Decode` and saved as PNG.
```csharp title="recipe-extract-images-from-pdf.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfDocument doc = PdfDocument.Open("manual.pdf");
for (int p = 0; p < doc.Pages.Count; p++)
{
IReadOnlyList images = doc.Pages[p].GetImages();
for (int i = 0; i < images.Count; i++)
{
PdfPageImage image = images[i];
string name = $"page{p + 1}-image{i + 1}";
if (image.Encoding == "DCTDecode")
{
File.WriteAllBytes(name + ".jpg", image.GetEncodedData()); // the JPEG as stored
}
else if (image.Decode() is PdfBitmap bitmap)
{
bitmap.SavePng(name + ".png"); // 8 bit gray or RGB pixels
}
Console.WriteLine($"{name}: {image.Width}x{image.Height}, {image.Encoding ?? "raw"}, {image.HorizontalResolution:0} dpi");
}
}
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/extract-images-from-pdf/
---
# Extract pages from a PDF in C#
Create an empty document with `PdfDocument.Create`, then import the pages you want with
`Pages.Import`. Page numbers in the sample start at 1. Fonts and images shared between the pages
are copied once.
```csharp title="recipe-extract-pdf-pages.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfDocument source = PdfDocument.Open("manual.pdf");
int[] keep = [1, 2, 5]; // page numbers, from 1, in the order you want them
PdfDocument extract = PdfDocument.Create();
extract.Pages.Import(keep.Select(n => source.Pages[n - 1]));
extract.Save("manual-extract.pdf");
Console.WriteLine($"manual-extract.pdf: {extract.Pages.Count} pages");
```
```bash
dotnet add package PdfNetStack
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/extract-pdf-pages/
---
# Fill and flatten a PDF form in C#
Look fields up by name in `doc.Form`, cast to the field type, and set the value. Setting a value
redraws the field's appearance, so the filled form looks the same in every viewer. `Flatten`
stamps the appearances into the pages and removes the form, so the values can no longer be edited.
```csharp title="recipe-fill-and-flatten-pdf-form.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfDocument doc = PdfDocument.Open("application.pdf");
((PdfTextField)doc.Form["name"]!).Text = "Jamal Abdelkhalek";
((PdfCheckBoxField)doc.Form["subscribe"]!).Checked = true;
((PdfRadioButtonField)doc.Form["plan"]!).Selected = "pro";
((PdfChoiceField)doc.Form["country"]!).Select("Morocco");
doc.Form.Flatten(); // values become page content, the form is removed
doc.Save("application-filled.pdf");
```

More: https://www.jadev-corp.com/pdfnetstack/recipes/fill-and-flatten-pdf-form/
---
# Flatten annotations in a PDF in C#
`PdfDocument.FlattenAnnotations` draws the appearance of every markup annotation (notes, text
markup, free text, stamps, ink, shapes and file attachments) into its page's content and removes
the annotation with its pop up. Links and form fields stay unless you ask for them.
```csharp
PdfDocument doc = PdfDocument.Open("contract-reviewed.pdf");
int flattened = doc.FlattenAnnotations(); // every page; links and form fields stay
doc.Save("contract-flat.pdf");
Console.WriteLine($"{flattened} annotations are now page content");
```
To flatten a single annotation, pass it to its page's collection:
```csharp
PdfDocument other = PdfDocument.Open("contract-reviewed.pdf");
PdfPage page = other.Pages[0];
PdfAnnotation stamp = page.Annotations.First(a => a.Subtype == "Stamp");
page.Annotations.Flatten(stamp); // only the stamp; the rest stay editable
other.Save("contract-stamped.pdf");
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/flatten-pdf-annotations/
---
# Convert an HTML invoice to PDF in C#
Pass the HTML to `HtmlConverter.ConvertToPdf` from the `PdfNetStack.Html` package. The `@page` rule
sets the paper size and margins, and its `@bottom-center` margin box numbers the pages. No browser
runs, and nothing is fetched from the network.
```csharp title="recipe-html-invoice-to-pdf.cs"
#:package PdfNetStack.Html@1.2.0
using PdfNetStack.Html;
string html = """
Invoice 42
Invoice 42
Issued 2026-10-29. Payment due within 30 days.
Item
Amount
Consulting, 8 hours
800.00 EUR
Support plan
200.00 EUR
Total
1,000.00 EUR
""";
byte[] pdf = HtmlConverter.ConvertToPdf(html);
File.WriteAllBytes("invoice-42.pdf", pdf);
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/html-invoice-to-pdf/
---
# Linearize a PDF for fast web view in C#
Set `Linearize = true` in `PdfOptimizerOptions` and optimize the file. The output is linearized as
ISO 32000-1 Annex F describes, with hint tables, and verified against the source like any other
rewrite. `Statistics.Linearized` tells you whether it was.
```csharp title="recipe-linearize-pdf.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
var options = new PdfOptimizerOptions { Linearize = true };
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("report.pdf", "report.web.pdf", options);
Console.WriteLine($"{result.Disposition}, linearized: {result.Statistics.Linearized}");
Console.WriteLine(result.Reason);
```
On the command line: `pdfnetstack optimize report.pdf report.web.pdf --linearize`.
More: https://www.jadev-corp.com/pdfnetstack/recipes/linearize-pdf/
---
# Make a scanned PDF searchable in C#
Load the OCR models with `OnnxOcrEngine.Load`, then call `PdfOcr.MakeSearchable` on the document and
save it. Each page's scan is found, turned upright, read, and every word is laid over it as
invisible text. The pages look the same as before.
```csharp title="recipe-make-scanned-pdf-searchable.cs"
#:package PdfNetStack.Ocr@1.2.0
using PdfNetStack;
using PdfNetStack.Ocr;
// Models: python tools/fetch_ocr_models.py models (checked against pinned SHA-256 hashes).
using var engine = OnnxOcrEngine.Load("models", new OnnxOcrEngineOptions { Languages = ["latin", "arabic"] });
PdfDocument doc = PdfDocument.Open("scan.pdf");
var ocr = new PdfOcr(engine, new PdfOcrOptions { Languages = ["latin", "arabic"] });
PdfOcrReport report = ocr.MakeSearchable(doc); // invisible text laid over each scan
doc.Save("scan-searchable.pdf");
Console.WriteLine(report.Text);
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/make-scanned-pdf-searchable/
---
# Merge PDF files in C#
Open each file with `PdfDocument.Open`, pass them to `PdfDocument.Merge`, and save the result.
Saving runs the lossless optimizer, so fonts and images the files share are written once.
```csharp title="recipe-merge-pdf-files.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
string[] files = ["a.pdf", "b.pdf", "c.pdf"];
PdfDocument merged = PdfDocument.Merge(files.Select(file => PdfDocument.Open(file)));
merged.Save("merged.pdf"); // shared fonts and images are written once
Console.WriteLine($"merged.pdf: {merged.Pages.Count} pages");
```
```bash
dotnet add package PdfNetStack
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/merge-pdf-files/
---
# Password protect a PDF in C#
Call `doc.Security.Encrypt` with a user password, an owner password and the permissions to grant,
then save. The user password opens the file with those permissions; the owner password gives full
access. New protection is AES-256 unless you pass `PdfEncryption.Aes128`.
```csharp title="recipe-password-protect-pdf.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfDocument doc = PdfDocument.Open("report.pdf");
doc.Security.Encrypt(
userPassword: "open-me", // needed to open the file
ownerPassword: "owner-secret", // needed to change it
permissions: PdfPermissions.Print | PdfPermissions.CopyContent); // AES-256 by default
doc.Save("report-protected.pdf");
PdfDocument check = PdfDocument.Open("report-protected.pdf", password: "open-me");
Console.WriteLine($"Opened with the user password: {check.Pages.Count} pages, {check.Security.Permissions}");
```
```bash
dotnet add package PdfNetStack
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/password-protect-pdf/
---
# Create a Factur-X invoice PDF in C#
Draw or convert the invoice, attach its XML with `PdfInvoice.Attach` and the profile it follows,
then save as PDF/A-3b. The XML is checked for well formedness and embedded as `factur-x.xml`
with the relationship the profile calls for, and saving writes the Factur-X XMP metadata with its
extension schema.
```csharp
PdfFont font = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2"); // PDF/A needs embedded fonts
PdfDocument doc = PdfDocument.Create();
doc.Info.Title = "Invoice 42";
doc.Language = "en";
PdfPage page = doc.Pages.Add(PdfRectangle.A4);
using (PdfGraphics g = page.CreateGraphics())
{
g.DrawString("Invoice 42", font, 24, PdfColor.Black, 72, 100);
g.DrawString("Total: 1,210.00 EUR", font, 12, PdfColor.Black, 72, 130);
}
// The invoice XML from your billing system, in Cross Industry Invoice syntax.
byte[] xml = File.ReadAllBytes("invoice-42.xml");
PdfInvoice.Attach(doc, xml, PdfInvoiceProfile.EN16931); // embedded as factur-x.xml
doc.Save("invoice-42.pdf", new PdfSaveOptions { Conformance = PdfConformance.PdfA3B });
PdfDocument check = PdfDocument.Open("invoice-42.pdf");
Console.WriteLine(string.Join(", ", check.Attachments.Select(a => a.Name)));
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/pdf-a-3-xml-attachment/
---
# Reduce PDF size for email in C#
Set the lossy image options on `PdfOptimizerOptions` and optimize as usual. `MaxImageDpi`
downsamples images drawn sharper than the page needs, `ConvertImagesToJpeg` stores raw
photographs as JPEG, and `JpegQuality` sets the quality of new JPEGs and requantizes existing ones.
Text, vectors, fonts, line art and screenshots are untouched, and the output is still verified
against the source.
```csharp
var options = new PdfOptimizerOptions
{
MaxImageDpi = 150, // no image sharper than the page needs
ConvertImagesToJpeg = true, // photographs stored raw become JPEG
JpegQuality = 80, // new JPEGs, and existing JPEGs requantized down to it
};
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("brochure.pdf", "brochure-email.pdf", options);
Console.WriteLine($"{result.Disposition}: {result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes, {result.SavingPercent:F1}% saved");
Console.WriteLine(result.Reason);
```
On the sample brochure, a lossless 1600 pixel photograph drawn about 6 inches wide, this takes the
file from 2,452,280 to 62,318 bytes.
More: https://www.jadev-corp.com/pdfnetstack/recipes/reduce-pdf-size/
---
# Remove a password from a PDF in C#
Open the file with its owner password, call `Security.RemoveEncryption`, and save. PdfNetStack
opens every standard security handler revision: RC4 40 to 128 bit, AES-128 and AES-256.
```csharp
PdfDocument doc = PdfDocument.Open("report-protected.pdf", password: "owner-secret");
doc.Security.RemoveEncryption();
doc.Save("report-unprotected.pdf");
```
```bash
dotnet add package PdfNetStack
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/remove-pdf-password/
---
# Rotate PDF pages in C#
Call `Rotate` on a page with a multiple of 90. The rotation adds to the page's current one and
viewers apply it when they display the page; the content itself is not rewritten. `Rotation` reads
the result: 0, 90, 180 or 270.
```csharp title="recipe-rotate-pdf-pages.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfDocument doc = PdfDocument.Open("report.pdf");
doc.Pages[0].Rotate(90); // clockwise, a multiple of 90
foreach (PdfPage page in doc.Pages.Skip(1))
{
page.Rotate(180);
}
doc.Save("report-rotated.pdf");
Console.WriteLine(string.Join(", ", doc.Pages.Select(p => p.Rotation)));
```
```bash
dotnet add package PdfNetStack
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/rotate-pdf-pages/
---
# Split a PDF into one file per page in C#
`PdfDocument.Split` returns one new document per page. Save each one under its own name. Inherited
page attributes such as resources, media box and rotation are materialized on each page, so every
file stands on its own.
```csharp title="recipe-split-pdf.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
PdfDocument doc = PdfDocument.Open("report.pdf");
int number = 1;
foreach (PdfDocument single in doc.Split()) // one new document per page
{
single.Save($"report-page-{number}.pdf");
number++;
}
Console.WriteLine($"Wrote {number - 1} files");
```
```bash
dotnet add package PdfNetStack
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/split-pdf/
---
# Convert a TIFF to PDF in C#
`PdfImage.GetPageCount` counts the pages of the TIFF and `PdfImage.Load(path, page)` loads one,
from 0. Add a PDF page the size of the image, which is its pixel size at the resolution the file
records, and draw the image at its natural size.
```csharp title="recipe-tiff-to-pdf.cs"
#:package PdfNetStack@1.2.0
using PdfNetStack;
using PdfNetStack.Drawing;
PdfDocument doc = PdfDocument.Create();
int pages = PdfImage.GetPageCount("scan.tif");
for (int i = 0; i < pages; i++)
{
PdfImage image = PdfImage.Load("scan.tif", i);
// A page the size of the image at its recorded resolution, in points.
PdfPage page = doc.Pages.Add(PdfRectangle.FromSize(image.Width, image.Height));
using PdfGraphics g = page.CreateGraphics();
g.DrawImage(image, 0, 0);
}
doc.Save("scan.pdf");
Console.WriteLine($"{pages} pages written");
```
More: https://www.jadev-corp.com/pdfnetstack/recipes/tiff-to-pdf/
---
# PDF command line tool for .NET
Install the tool once, then run a command per job. `optimize` is lossless by default and returns
the original bytes when a rewrite would not be verified equivalent or would save less than 1%.
```bash
dotnet tool install -g PdfNetStack.Tool
pdfnetstack optimize in.pdf out.pdf
pdfnetstack batch ./incoming ./optimized --report report.csv
```
It needs the .NET 8 or later runtime. `pdfnetstack --help` prints every command and option.
More: https://www.jadev-corp.com/pdfnetstack/cli/
---
# Frequently asked questions
> Answers about PdfNetStack licensing, platforms, native dependencies, lossless compression, HTML to PDF, Arabic text, PDF/A, OCR and memory use.
Short answers to the questions people ask before they adopt PdfNetStack. Each one links to the
page with the details below.
## Where to read more
- Licensing and dependencies: [About](https://www.jadev-corp.com/about/)
- Platforms and installation: [Getting started](https://www.jadev-corp.com/getting-started/)
- Compression results and method: [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/), [Benchmarks](https://www.jadev-corp.com/benchmarks/),
[Fidelity and verification](https://www.jadev-corp.com/concepts/fidelity/)
- Encrypted, signed and damaged files: [Safety model](https://www.jadev-corp.com/concepts/safety-model/)
- HTML to PDF: [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) and [CSS support](https://www.jadev-corp.com/guides/html-to-pdf/css-support/)
- Arabic and right to left text: [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/)
- PDF/A: [PDF/A](https://www.jadev-corp.com/guides/pdf-a/)
- OCR: [OCR](https://www.jadev-corp.com/guides/ocr/)
- Memory: [Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/)
- What comes next: [Roadmap](https://www.jadev-corp.com/roadmap/)
A question that is not answered here? Ask it in the
[issues](https://github.com/jams4code/pdf.netstack/issues) of the repository.
## Is PdfNetStack free for commercial use?
Yes. PdfNetStack is MIT licensed. On .NET 8 and later the core package has no dependencies; on .NET Standard 2.0 it uses Microsoft's System.Memory, which is also MIT. CI blocks any runtime package that is not on a reviewed license allowlist.
## Does it need Ghostscript, a browser or other native binaries?
No. The library, the HTML to PDF package and the command line tool are managed code and start no external process. The one exception is OCR. The optional OCR add-on and its pdfnetstack-ocr tool run their models on ONNX Runtime, and pdfnetstack ocr starts that separate tool.
## Which platforms and .NET versions does it run on?
.NET 8, 9, 10 and later through the net8.0 and net10.0 targets, and .NET Framework 4.6.2 to 4.8.1, Mono, Unity and .NET Core 2.0+ through netstandard2.0. CI runs the tests on Windows, Linux and macOS, on .NET Framework 4.8, .NET 8 and .NET 10.
## Is it compatible with trimming and Native AOT?
Yes. The net8.0 and net10.0 builds of the library are marked trim and AOT compatible, and CI publishes the command line tool with Native AOT, failing on any trim or AOT warning.
## Is the compression lossless?
Yes, by default. Text, vectors, fonts, images, annotations, forms, bookmarks, structure tags, metadata and attachments are carried over, and every output is compared to its source before it is returned. Lossy changes happen only when you ask for them, as JPEG requantization with JpegQuality, and from 1.1 image downsampling with MaxImageDpi and conversion of raw photographs to JPEG, both checked against the source and optionally held back by a perceptual quality gate.
## How much smaller do files get?
On a mixed corpus of 1,296 real world files, the 1,003 files that were optimized went from 568.9 MiB to 469.0 MiB, 17.6% saved, with zero differing pixels at 150 DPI. Image heavy files often gain 20 to 40%. Files from producers that already use object streams gain little.
## What happens with encrypted or signed PDFs?
The optimizer returns them byte for byte, because rewriting would break the encryption or the signatures. The editing API opens encrypted files with a password and keeps their protection on save. Saving a signed document in full invalidates its signatures; from version 1.1, SaveIncremental appends the changes after the signed bytes and keeps them valid.
## Can PdfNetStack render PDF pages to images?
No. Rendering pages to images is out of scope for the project. It extracts the images a page contains, and the text, but it does not rasterize pages.
## Is the HTML to PDF converter a headless browser?
No. PdfNetStack.Html is a layout engine written in C# with the CSS cascade, block, inline, table, flex and grid layout and paged media. It does not run JavaScript, so pages must be rendered HTML. It ships as the PdfNetStack.Html package on NuGet.
## Does it handle Arabic, Indic and right to left text?
Yes, from version 1.1. Text goes through the Unicode bidirectional algorithm, Unicode normalization and an OpenType shaper, so Arabic letters join and take their contextual forms, the nine major Indic scripts reorder and form their conjuncts, and right to left text copies and searches in logical order. Thai, Khmer, Myanmar and Hangul shaping are on the roadmap.
## Can it create PDF/A files?
Yes, from version 1.1. Saving with PdfSaveOptions.Conformance writes PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e or 4f, after checking what cannot be repaired and listing every problem at once. PdfInvoice adds Factur-X and ZUGFeRD invoice data to PDF/A-3 files, and CI validates the output with veraPDF.
## Does it support long term signatures?
Yes, from version 1.1. PdfNetStack signs to PAdES B-B, B-T, B-LT and B-LTA, and validates signatures against the trust anchors you supply. It makes no network calls; your application sends the timestamp requests and supplies the revocation data.
## Can it make scanned PDFs searchable?
Yes, with the PdfNetStack.Ocr add-on, for Latin and Arabic script. It is a separate package on NuGet, with its models in PdfNetStack.Ocr.Models. The core library also has DrawInvisibleText to lay the words of any OCR engine over a scan.
## How much memory does it use?
The optimizer holds the whole document in memory. Plan for about 10 times the file size, more for image heavy files, or lower MaxDegreeOfParallelism to cap memory. From version 1.1, PdfDocument.OpenOnDemand reads and edits a file of any size page by page without loading it, and SaveIncremental appends only the changes.
## Can it write linearized PDFs for fast web view?
Yes, from version 1.1, with Linearize in PdfOptimizerOptions or pdfnetstack optimize --linearize. The output follows ISO 32000-1 Annex F with hint tables and is verified like any other rewrite.
## Is it safe to run on files from untrusted sources?
That is a design goal. Nesting depth, decoded stream sizes, image sizes and loops in the file structure are bounded, and damaged input raises PdfFormatException instead of crashing or hanging. The HTML converter fetches nothing from the network unless you give it a resource loader.
## Which version should I use today?
Version 1.2.0, on NuGet. It has every feature on this site, including the optimizer, editing, drawing, fonts, forms, signatures, PDF/A, extraction, HTML to PDF, OCR and the command line tools.
## Is PdfNetStack related to the PDFNet SDK?
No. PDFNet is a commercial SDK from Apryse. PdfNetStack is an independent open source project by JADEV with no connection to it.
## Who maintains PdfNetStack?
Jamal Abdelkhalek at JADEV created it and maintains it. Contributions are welcome under the contribution guide in the repository.
Source: https://www.jadev-corp.com/pdfnetstack/faq/
---
# Run PdfNetStack on AWS Lambda
Publish the function with Native AOT for the `provided.al2023` runtime. Package the fonts your
HTML uses with the function, load them once per instance, and turn installed fonts off. Create
the options once and share them: every invocation the instance serves uses them.
```csharp
// Created once per instance and shared by every invocation, concurrent ones included.
internal static class Pdfs
{
private static readonly HtmlConverterOptions Options = new()
{
Fonts = LoadFonts(),
UseSystemFonts = false, // the host has no fonts, or ones that can change
MaxPages = 200,
};
public static byte[] Convert(string html) => HtmlConverter.ConvertToPdf(html, Options);
// The fonts deployed with the function, in fonts/ next to the executable.
private static PdfFontCollection LoadFonts()
{
var fonts = new PdfFontCollection();
fonts.AddDirectory(Path.Combine(AppContext.BaseDirectory, "fonts"));
return fonts;
}
}
```
A complete function with an S3 trigger, a function URL and a SAM template is in
[samples/serverless/aws-lambda](https://github.com/jams4code/pdf.netstack/tree/main/samples/serverless/aws-lambda).
More: https://www.jadev-corp.com/pdfnetstack/guides/aws-lambda/
---
# Run PdfNetStack on Azure Functions
Package the fonts your HTML uses with the app, load them once, and turn installed fonts off.
Create the options once: every invocation the worker runs shares them, concurrent ones included.
Publish ready to run.
```csharp
// Created once per instance and shared by every invocation, concurrent ones included.
internal static class Pdfs
{
private static readonly HtmlConverterOptions Options = new()
{
Fonts = LoadFonts(),
UseSystemFonts = false, // the host has no fonts, or ones that can change
MaxPages = 200,
};
public static byte[] Convert(string html) => HtmlConverter.ConvertToPdf(html, Options);
// The fonts deployed with the function, in fonts/ next to the executable.
private static PdfFontCollection LoadFonts()
{
var fonts = new PdfFontCollection();
fonts.AddDirectory(Path.Combine(AppContext.BaseDirectory, "fonts"));
return fonts;
}
}
```
A complete app with an HTTP trigger for HTML and an Event Grid blob trigger for optimization is
in [samples/serverless/azure-functions](https://github.com/jams4code/pdf.netstack/tree/main/samples/serverless/azure-functions).
More: https://www.jadev-corp.com/pdfnetstack/guides/azure-functions/