# 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
> Shrink PDFs losslessly in .NET with object streams, deduplication, resource pruning and JPEG re-encoding, every output verified. MIT, no Ghostscript.
Available since 1.0.
PdfNetStack makes PDF files smaller without changing a rendered pixel, then proves it before it
hands you the result.
## Short answer
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/).
## What the result tells you
`PdfOptimizationResult` reports what was written and why.
```csharp
Console.WriteLine($"Disposition: {inMemory.Disposition}");
Console.WriteLine($"Size: {inMemory.OriginalBytes:N0} to {inMemory.OutputBytes:N0} bytes ({inMemory.SavingPercent:F1}%)");
Console.WriteLine($"Pages: {inMemory.PageCount}, output SHA-256: {inMemory.OutputSha256}");
Console.WriteLine($"Reason: {inMemory.Reason}");
Console.WriteLine($"JPEG images requantized: {inMemory.Statistics.JpegImagesRequantized}");
```
| Property | Meaning |
|---|---|
| `Disposition` | `Optimized` (a smaller, verified rewrite), `OriginalRetained` (no worthwhile saving, or verification failed) or `UnsupportedRetained` (refused by the safety rules) |
| `OriginalBytes`, `OutputBytes`, `SavingPercent` | Sizes before and after |
| `SourceSha256`, `OutputSha256` | Hashes of the input and of what was written |
| `PageCount` | Pages in the document |
| `Reason` | A sentence that explains the disposition and names every lossy change |
| `Statistics` | Per pass counters and timings, such as objects merged, JPEG images re-encoded and images downsampled |
The output is always a valid PDF: either the verified rewrite or the input unchanged.
## Bytes, streams and files
Three entry points cover the usual cases. All of them take the same options and return the same
result.
```csharp
// In memory
(byte[] output, PdfOptimizationResult inMemory) = PdfOptimizer.Optimize(File.ReadAllBytes("manual.pdf"), options);
File.WriteAllBytes("manual.small.pdf", output);
// Streams
await using (FileStream input = File.OpenRead("manual.pdf"))
await using (FileStream destination = File.Create("manual.stream.pdf"))
{
PdfOptimizationResult streamed = await PdfOptimizer.OptimizeAsync(input, destination);
Console.WriteLine($"Streams: {streamed.Disposition}, {streamed.SavingPercent:F1}% saved");
}
```
`OptimizeFileAsync` writes the output file. `OptimizeAsync` reads a stream and writes another one,
which suits blob storage and HTTP bodies. `Optimize` works on a byte array and returns the bytes.
## Options
Pass a `PdfOptimizerOptions` to change the defaults.
```csharp
var options = new PdfOptimizerOptions
{
Compression = CompressionEffort.Balanced, // faster, slightly larger
AllowIncrementalUpdates = true, // flatten edit history into one revision
MaxDegreeOfParallelism = 2, // cap CPU and memory per document
JpegQuality = 60, // opt-in, lossy: requantize JPEG images
};
```
| Option | Default | Notes |
|---|---|---|
| `MinSavingPercent` | 1.0 | Keep the original unless the rewrite saves at least this much |
| `Compression` | `Maximum` | `Fast` and `Balanced` trade size for speed: `Balanced` cut corpus time by about a third for about 2% larger output. `Exhaustive` (1.1) searches harder, see below |
| `UseObjectStreams` | true | Off writes a classic xref table |
| `ObjectsPerStream` | 200 | Larger compresses better, smaller loads single pages faster |
| `DeduplicateObjects` | true | Merges identical fonts, images, ICC profiles, resources and arrays; from 1.1 streams compare by decoded content |
| `PruneUnusedResources` | true | 1.1. Removes resource entries no content names |
| `CompactContentStreams` | true | 1.1. Rewrites content streams with minimal white space, same operators and operands |
| `MergeFontSubsets` | true | 1.1. Merges subsets of one TrueType font that keep the original glyph ids. Needs `DeduplicateObjects` |
| `Linearize` | false | 1.1. Fast web view. No object streams, so often larger; kept anyway for input not yet linearized |
| `RecompressStreams` | true | Recompresses Flate at zlib level 9, turns LZW and RunLength into Flate |
| `UseImagePredictors` | true | Tries PNG predictors on raw images |
| `OptimizeJpegImages` | true | Lossless JPEG re-encoding |
| `ProgressiveJpeg` | false | 1.1. Also tries progressive coding for every JPEG written, kept when smaller. Lossless |
| `JpegQuality` | null | 1 to 100, libjpeg scale. Lossy: requantizes JPEG images, and sets the quality of new JPEGs |
| `MaxImageDpi` | null | 1.1. Downsamples images drawn above 1.5 times this resolution. Lossy |
| `MaxBilevelImageDpi` | null | 1.2. The same for bilevel images, in place of `MaxImageDpi`; null uses `MaxImageDpi`. Lossy |
| `ConvertImagesToJpeg` | false | 1.1. Photographic raw images to JPEG at `JpegQuality`. Lossy |
| `MinImageSsim` | null | 1.1. Quality gate, 0 to 1: any lossy image change below this SSIM is dropped |
| `Verify` | true | Disable only for inputs you already trust |
| `AllowIncrementalUpdates` | false | Flattens edit history into one revision |
| `AllowRepairedFiles` | false | Rewrites damaged files into clean ones |
| `RemovePageThumbnails` | false | Not lossless |
| `RemovePieceInfo` | false | Not lossless for the authoring application, invisible to viewers |
| `MaxInputBytes` | 1 GiB | Larger inputs are returned unchanged |
| `MaxDecodedStreamBytes` | 256 MiB | Per stream decode ceiling, a zip bomb defence |
| `MaxDegreeOfParallelism` | CPU count | Lower it to cap memory on image heavy files |
## What the optimizer does
| Pass | Effect | Lossless |
|---|---|---|
| Garbage collection | Drops objects unreachable from the catalog, including old revisions | Yes |
| Object streams | Packs non-stream objects into Flate compressed object streams (PDF 1.5) | Yes |
| Xref stream | Replaces the text xref table with a predictor encoded, compressed stream | Yes |
| Stream recompression | Removes ASCIIHex and ASCII85 layers, turns LZW and RunLength into Flate, recompresses Flate at level 9, keeps a result only when smaller | Yes |
| Image prediction | Tries PNG predictors on raw images of 8 bits or less per component | Yes |
| JPEG re-encoding | Rebuilds Huffman tables per image, writes progressive and restart marked JPEGs as baseline, drops segments PDF readers ignore. The DCT coefficients stay identical | Yes |
| Deduplication | Merges identical objects, iterating until merges stop. From 1.1, streams compare by what they decode to, so differently encoded copies merge | Yes |
| Unused resources | `PruneUnusedResources` (1.1): fonts, XObjects, graphics states, patterns, shadings, color spaces and property lists that no content names are removed. Anything that cannot be read for certain is kept | Yes |
| Content streams | `CompactContentStreams` (1.1): page, form, pattern and appearance content rewritten with minimal white space, canonical numbers, shortest strings and no comments; inline image data copied exactly | Yes |
| Font subsets | `MergeFontSubsets` (1.1): separate subsets of one TrueType font, as merged documents carry them, become one program when no glyph any page shows changes | Yes |
| Linearization | `Linearize` (1.1): first page first, two cross-reference sections and hint tables (Annex F), for fast web view | Yes, opt-in |
| Compact syntax | Minimal separators, canonical numbers, null entries dropped | Yes |
| Progressive JPEG | `ProgressiveJpeg`: every JPEG written is also tried progressive, kept when smaller | Yes, opt-in |
| Exhaustive Flate | `Compression = Exhaustive`: optimal parsing and block splitting in the manner of Zopfli, zlib kept when smaller | Yes, opt-in |
| Thumbnails, PieceInfo | Optional removal of page thumbnails and application private data | No, opt-in |
| JPEG requantization | `JpegQuality`: coarser quantization, never finer than the image's own. Soft masks stay lossless | No, opt-in |
| Downsampling | `MaxImageDpi`: images drawn above 1.5 times the target resolution are resampled with a Lanczos filter | No, opt-in |
| Photographs to JPEG | `ConvertImagesToJpeg`: raw gray and RGB images that look photographic become baseline JPEG | No, opt-in |
| Quality gate | `MinImageSsim`: any lossy image change below this luminance SSIM is dropped | Opt-in |
Without the lossy image options, JPEG data is decoded to DCT coefficients and never to pixels, so
components, chroma sampling and the color model pass through untouched: a CMYK image stays CMYK.
JPEG 2000, JBIG2 and CCITT data is never decoded or touched by the optimizer.
## Lossy image options
> **Available since 1.1:** `MaxImageDpi`, `ConvertImagesToJpeg`, `QuantizeImages`, `MinImageSsim`, `ProgressiveJpeg`, the
> `Balanced` and `Smallest` presets and `CompressionEffort.Exhaustive` arrived in 1.1. `JpegQuality`
> is in 1.0.
All of them are off by default. For most documents a preset is all you need:
`PdfOptimizerOptions.Balanced` gives much smaller files with no change most readers can see, and
`PdfOptimizerOptions.Smallest` goes further for reading on screen, with images at 150 dpi and
bilevel scans at 300, where text stays clean. To choose for yourself, turn the
options on together for the biggest saving on image heavy files:
```csharp
var options = new PdfOptimizerOptions
{
MaxImageDpi = 150, // downsample images drawn above 225 dpi to 150 dpi
ConvertImagesToJpeg = true, // photographs stored raw become JPEG
JpegQuality = 90, // new JPEGs, and existing ones finer than this are requantized
MinImageSsim = 0.95, // keep any image a change would visibly hurt
ProgressiveJpeg = true, // lossless: progressive coding when smaller
};
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("brochure.pdf", "brochure-small.pdf", options);
```
- `MaxImageDpi` measures each image at the largest size it is drawn anywhere: on pages, in forms,
tiling patterns, soft mask groups and annotation appearances. An image drawn above 1.5 times the
target is resampled to the target with a three lobe Lanczos filter. A downsampled JPEG stays a
JPEG, encoded at `JpegQuality` but never finer than it was; raw samples stay lossless after
resampling unless `ConvertImagesToJpeg` is set.
- `ConvertImagesToJpeg` re-encodes raw 8 bit gray and RGB images as baseline JPEG at `JpegQuality`
(4:2:0 below quality 90, Huffman tables optimized per image). A classifier leaves line art,
screenshots, charts, palette and bilevel images lossless, and a JPEG is kept only when it is at
least 10% smaller than the best lossless encoding. It requires `JpegQuality`.
- `QuantizeImages` reduces raw 8 bit RGB images to a palette of at most 256 colors, in their own
color space. This is the option for technical drawings, charts and screenshots: JPEG rings around
their fine labels and often saves little, while a palette keeps edges sharp and Flate then
compresses the flat areas well. Each image gets the smallest palette, of 256, 64, 16 or 4 colors,
that keeps `MinImageSsim` (0.97 without a gate), changes pixels by at most 8 sample steps on
average, and changes at most 1% of them by more than 32. Colors that dominate a cluster stay
exact, so a white background stays white. With `ConvertImagesToJpeg` as well, the smaller result
wins image by image, so set `MinImageSsim` if you want JPEG only where it looks right. Images
with `/Decode`, `/Interpolate` or a `/Matte` soft mask are left alone.
- `MinImageSsim` is a perceptual gate on luminance SSIM. Any image whose lossy change,
requantization included, falls below it stays as it was, and `ImagesKeptByQualityGate` counts
those images. Around 0.95 keeps changes hard to see; 0.90 allows visible softening of fine
texture.
- `ProgressiveJpeg` changes the coding, not the coefficients, so it is lossless.
The result says what changed:
```csharp
OptimizationStatistics stats = result.Statistics;
Console.WriteLine($"{result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes ({result.SavingPercent:F1}% saved)");
Console.WriteLine($"Downsampled: {stats.ImagesDownsampled}, converted to JPEG: {stats.ImagesConvertedToJpeg}, " +
$"requantized: {stats.JpegImagesRequantized}, kept by the quality gate: {stats.ImagesKeptByQualityGate}");
Console.WriteLine(result.Reason);
```
`OptimizationStatistics` reports `ImagesDownsampled`, `ImagesConvertedToJpeg`, `ImagesQuantized`,
`JpegImagesRequantized` and `ImagesKeptByQualityGate`, and `Reason` names every lossy change, for
example "1 image(s) downsampled to 150 dpi, 1 image(s) converted to JPEG at quality 90".
### What the proof checks for changed images
Verification still runs on every image the optimizer changed, but for pixels that changed it is a
bound, not an identity. A downsampled or converted image must keep its components and aspect
ratio, must not grow, and is decoded and compared with the source resampled to its new size: each
channel's mean, a coarse thumbnail that catches mirrored, shifted or swapped pictures, and
luminance SSIM of at least `MinImageSsim` (0.5 when no gate is set). Soft masks, masks, stencil
masks and color keyed images must match exactly, and every other part of the document must be
identical as before. The method is on the [fidelity](https://www.jadev-corp.com/concepts/fidelity/) page.
### What is left alone
Masks, images with a color key mask or a /Matte soft mask, bilevel, indexed, 16 bit, Lab,
Separation and DeviceN images keep their size, and so do images reachable from anything the
measurement does not walk, such as Type 3 glyphs and thumbnails. JPEG 2000, JBIG2 and CCITT images
are never changed. A JPEG with a color key mask is never requantized, since the key matches exact
sample values.
These options were measured on synthetic files made by `tools/make_lossy_corpus.py`, not on a real
corpus. The corpus results below are for the lossless passes only.
## Structural passes
Three lossless passes added in 1.1 work on the document's structure rather than on its streams'
compression, and all three are on by default. Unused resources go: merged and edited files often
list fonts and images in a page's resources that no content on it uses. Content streams are
written compactly. Subsets of one TrueType font that merging tools embedded once per document are
merged into one program when every glyph stays the same. The statistics count each of them.
```csharp
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("merged.pdf", "merged.optimized.pdf");
OptimizationStatistics stats = result.Statistics;
Console.WriteLine($"{result.Disposition}, saved {result.SavingPercent:F1}%");
Console.WriteLine($"Resources pruned: {stats.ResourcesPruned}");
Console.WriteLine($"Content streams compacted: {stats.ContentStreamsCompacted}");
```
None of them is taken on trust. The verifier runs its own analysis of the source and accepts a
missing resource only where no content can name it, a rewritten content stream only when it reads
as the same tokens, and a merged font only when every glyph shown keeps its outline and metrics.
The rules are on the [fidelity](https://www.jadev-corp.com/concepts/fidelity/) page. Turn a pass off with its option, or
`--no-prune`, `--no-compact` and `--no-font-merge` on the command line.
## Fast web view
`Linearize = true` writes a linearized file (ISO 32000-1, Annex F): the first page and what it
needs come first, with hint tables, so a viewer can show it before the whole file has arrived.
Inherited page attributes move onto the pages, and every object is written on its own, without
object streams, so the output can be larger than the input. A linearized rewrite of a file that
is not linearized yet is kept even then, provided it verifies. `Statistics.Linearized` says
whether the output is linearized; a page tree that cannot be laid out page by page, such as one
that lists a page twice, is written as usual and the reason says why.
```csharp
var webView = new PdfOptimizerOptions { Linearize = true };
PdfOptimizationResult linear = await PdfOptimizer.OptimizeFileAsync("manual.pdf", "manual.web.pdf", webView);
Console.WriteLine($"Linearized: {linear.Statistics.Linearized}, {linear.OriginalBytes:N0} to {linear.OutputBytes:N0} bytes");
```
The tests read every linearized file back and check the linearization dictionary, both
cross-reference sections and every hint table entry against the real offsets.
`tools/measure_optimizer.py --linearize` also runs qpdf's linearization check when qpdf is
installed.
## Exhaustive Flate
`CompressionEffort.Exhaustive` is an original Deflate encoder in the manner of Zopfli: optimal
parsing over every match length with costs refined over several passes, block splitting, and code
lengths limited by package merge. Output stays standard Flate, and zlib's result is kept whenever
it is smaller. On the synthetic files of `tools/make_lossy_corpus.py` it saved 3 to 5% more than
level 9 on text, scans and photographs, and nothing on line art and screenshots, for 2 to 150
times the CPU time. It suits archives written once and read often.
```csharp
// Lossless, for archives: a slower Deflate search, kept only where it beats zlib.
var archival = new PdfOptimizerOptions { Compression = CompressionEffort.Exhaustive };
PdfOptimizationResult archived = await PdfOptimizer.OptimizeFileAsync("manual.pdf", "manual-archive.pdf", archival);
Console.WriteLine($"Exhaustive: {archived.OriginalBytes:N0} to {archived.OutputBytes:N0} bytes");
```
## Files the optimizer will not rewrite
The optimizer returns the input unchanged, with `UnsupportedRetained` and a reason, when the file
is encrypted, digitally signed or carries usage rights, incrementally updated (unless
`AllowIncrementalUpdates` is set), damaged and repaired on load (unless `AllowRepairedFiles` is
set), or larger than `MaxInputBytes`. The [safety model](https://www.jadev-corp.com/concepts/safety-model/) explains each
rule.
## Check another tool's output
The equivalence check the optimizer runs on itself is public. Use it on the output of any PDF
tool:
```csharp
byte[] original = File.ReadAllBytes("manual.pdf");
byte[] candidate = File.ReadAllBytes("manual.checked.pdf"); // written by any tool
string? difference = PdfEquivalence.FindDifference(original, candidate);
Console.WriteLine(difference ?? "Equivalent: same objects, same decoded content.");
```
`FindDifference` returns `null` when the two documents are equivalent, or a description of the
first difference. An overload takes `allowJpegRequantization` and `minImageSsim`, and then also
accepts requantized, downsampled and converted images within the bounds above:
```csharp
// The same check on any tool's output: accept downsampled and converted images that keep SSIM 0.95.
string? difference = PdfEquivalence.FindDifference(
File.ReadAllBytes("brochure.pdf"), File.ReadAllBytes("brochure-small.pdf"),
maxDecodedStreamBytes: 256L << 20, allowJpegRequantization: true, minImageSsim: 0.95);
Console.WriteLine(difference ?? "Within the bounds of the lossy proof.");
```
The command line equivalent is `pdfnetstack verify original.pdf other.pdf --min-ssim 0.95`. The
method and its limits are on the [fidelity](https://www.jadev-corp.com/concepts/fidelity/) page.
## Results
Measured on a mixed real world corpus of 1,296 files (569 MiB) with default options, lossless, on
an 8 core Windows machine:
| | |
|---|---|
| Optimized | 1,003 files, 568.9 MiB to 469.0 MiB (17.6% saved) |
| Returned unchanged | 38 below the 1% threshold, 130 refused by the safety rules, 125 not PDFs |
| Verification failures | 0 |
| Throughput | about 65 s for the whole corpus, 8 to 9 MB/s |
| With `AllowIncrementalUpdates` and `AllowRepairedFiles` | 1,118 files optimized, 20.1% saved |
Every optimized file was rendered with PDFium at 150 DPI and compared with its original: 2,161
pages, zero differing pixels. See [Benchmarks](https://www.jadev-corp.com/benchmarks/) for the method. These figures were
measured with version 1.0, before the structural passes of 1.1, which have so far been measured
on synthetic files (`tools/make_optimizer_samples.py` and `tools/measure_optimizer.py`).
## Limits
- Lossy image options handle 8 bit gray, RGB and CMYK images, raw or JPEG. JPEG 2000, JBIG2,
CCITT, 16 bit, indexed, Lab, Separation and DeviceN images keep their resolution, and images
inside Type 3 glyphs are never downsampled. The quality gate measures luminance only.
- Arithmetic coded, 12 bit, lossless and hierarchical JPEGs, JPEGs with more than 64 scans, and
damaged JPEG data are carried over untouched.
- The optimizer does not subset fonts. It merges subsets of one font only when they keep the
original glyph ids.
- Linearization is opt-in. Without `Linearize`, the linearization of an input is dropped.
- The optimizer holds the whole document in memory: plan for about 10 times the file size, see
[Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/). To read or edit a large file
without loading it, see [Large files](https://www.jadev-corp.com/guides/large-files/).
## Related
- [Command line](https://www.jadev-corp.com/cli/): `optimize`, `batch`, `inspect` and `verify`
- [Large files](https://www.jadev-corp.com/guides/large-files/): open on demand and save incremental updates
- [Linearize a PDF for fast web view](https://www.jadev-corp.com/recipes/linearize-pdf/)
- [Reduce PDF size for email](https://www.jadev-corp.com/recipes/reduce-pdf-size/) and
[Downsample images in a PDF](https://www.jadev-corp.com/recipes/downsample-pdf-images/)
- [Edit pages](https://www.jadev-corp.com/guides/edit-pages/): saving an edited document runs the optimizer too
- [Fidelity and verification](https://www.jadev-corp.com/concepts/fidelity/)
- API: [PdfOptimizer](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizer/), [PdfOptimizerOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizeroptions/), [PdfOptimizationResult](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizationresult/), [OptimizationStatistics](https://www.jadev-corp.com/api/pdfnetstack/optimizationstatistics/), [PdfEquivalence](https://www.jadev-corp.com/api/pdfnetstack/pdfequivalence/)
## Is PdfNetStack compression lossless?
Yes, by default. Text, vectors, fonts, images, forms, tags and metadata are carried over, and every output is compared to its source before it is returned. The lossy image options (JPEG requantization, downsampling, JPEG conversion) are opt-in.
## How much smaller do PDFs get?
On a mixed corpus of 1,296 real world files the optimized ones went from 568.9 MiB to 469.0 MiB, 17.6% saved, losslessly. Image heavy files often gain 20 to 40%. Files from producers that already use object streams gain little.
## Can it downsample images?
Yes, from version 1.1, with MaxImageDpi. Images drawn above 1.5 times the target resolution are resampled with a Lanczos filter, and MinImageSsim keeps any image the change would visibly hurt.
## Can PdfNetStack write linearized PDFs for fast web view?
Yes, from version 1.1, with Linearize = true or pdfnetstack optimize --linearize. The file is laid out first page first with hint tables, as ISO 32000-1 Annex F describes, and verified like any other rewrite.
## Does the optimizer need Ghostscript or another native tool?
No. It is one managed assembly with no native code and no external process, on Windows, Linux and macOS.
Source: https://www.jadev-corp.com/pdfnetstack/guides/compress-pdf/
---
# Edit PDF pages in C# and .NET
> Merge, split, reorder, rotate, insert and remove PDF pages in C#, edit metadata and bookmarks, and save an optimized, verified file. MIT licensed.
Available since 1.0.
Open a PDF, change its pages, metadata and bookmarks, and save a new file that is optimized and
verified on the way out.
## Short answer
`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/).
## Open and create
| Call | What it does |
|---|---|
| `PdfDocument.Open(path, password)` | Reads a file. The password is only needed for encrypted files |
| `PdfDocument.Open(stream, password)` | Reads a stream to its end |
| `PdfDocument.Open(bytes, password, maxDecodedStreamBytes)` | Opens from memory. The buffer is referenced, not copied, so it must not change while the document is in use |
| `PdfDocument.Create()` | An empty document. Add at least one page before saving |
A file that is not a readable PDF raises `PdfFormatException`. A wrong password raises
`PdfPasswordException`, see [Encryption](https://www.jadev-corp.com/guides/encryption/).
## Pages
`doc.Pages` is a `PdfPageCollection`, zero based.
| Member | Effect |
|---|---|
| `Add(PdfRectangle.A4)`, `Insert(index, size)` | Blank page at the end or at a position |
| `RemoveAt(index)`, `Remove(page)`, `Clear()` | Removes pages. Links and bookmarks pointing to them become empty |
| `Move(from, to)` | Reorders |
| `Import(pages, index)` | Copies pages from this or another document to the end, or to `index`, and returns the new pages |
| `page.Rotate(90)` | Rotates clockwise by a multiple of 90. `page.Rotation` reads the result: 0, 90, 180 or 270 |
| `page.Width`, `page.Height`, `page.MediaBox`, `page.CropBox` | Page geometry, in points |
Inherited page attributes (resources, media box, rotation) are copied onto each page when the
document opens, so a page stays correct when it moves within the document or to another one.
## Merge, split and extract
```csharp
PdfDocument merged = PdfDocument.Merge([
PdfDocument.Open("a.pdf"),
PdfDocument.Open("b.pdf"),
PdfDocument.Open("c.pdf"),
]);
merged.Save("merged.pdf");
```
`Merge` imports every page of every document, in order. `Split` returns one new document per page:
```csharp
int number = 0;
foreach (PdfDocument single in PdfDocument.Open("report.pdf").Split())
{
single.Save($"report-page-{++number}.pdf");
}
```
To extract a page range, import the pages you want into a new document. Any order works, and the
same page may appear twice:
```csharp
PdfDocument source = PdfDocument.Open("manual.pdf");
PdfDocument excerpt = PdfDocument.Create();
excerpt.Pages.Import([source.Pages[4], source.Pages[0], source.Pages[1]]); // any order
excerpt.Save("manual-excerpt.pdf");
```
Annotations travel with their page and are copied fresh for every import. Links to pages imported
in the same call are remapped to the copies; links to pages left behind become empty. Since 1.1,
form fields travel with their pages too, renamed with a `_2` suffix when the target form already
has the name, see [Forms](https://www.jadev-corp.com/guides/forms/#fields-in-imported-pages). A document
opened with only its user password gives up its pages only when its permissions allow copying.
## Metadata
`doc.Info` holds the document information dictionary: title, author, subject, keywords, creator,
producer and the two dates, plus any custom entry by name.
```csharp
Console.WriteLine($"Title: {doc.Info.Title}, PDF {doc.Version}, {doc.Pages.Count} pages");
doc.Info.Title = "Quarterly report, final";
doc.Info.Author = "Finance";
doc.Info.Keywords = "report, quarterly";
doc.Info.ModificationDate = DateTimeOffset.Now;
doc.Info["Department"] = "Controlling"; // custom entries too
foreach (string key in doc.Info.Keys)
{
Console.WriteLine($" {key}: {doc.Info[key]}");
}
```
Values are decoded from PDFDocEncoding or UTF-16 and written back in whichever of the two fits.
Changing them does not update XMP metadata; [PDF/A](https://www.jadev-corp.com/guides/pdf-a/) output keeps both in sync.
## Bookmarks
`doc.Outlines` reads and writes the bookmark tree. A bookmark has a title, the page it opens, and
child bookmarks. Existing bookmarks keep their original styling and actions when you edit others.
```csharp
PdfOutline summary = doc.Outlines.Add("Summary", doc.Pages[0]);
summary.Add("Revenue", doc.Pages[1]); // nested under Summary
summary.Add("Costs", doc.Pages[2]);
summary.IsOpen = true; // expanded when the file opens
foreach (PdfOutline bookmark in doc.Outlines)
{
Console.WriteLine($"{bookmark.Title}: page {bookmark.Destination?.Index + 1}, {bookmark.Children.Count} children");
}
```
## Named destinations
Available since 1.1. Bookmarks and links often point at a name instead of a page, and the
document maps each name to a page. `doc.NamedDestinations` lists the names of the name tree and of
the older catalog dictionary, resolves them to pages, and renames them. A rename changes every
bookmark, link and GoTo action that refers to the name, in its own string or name form, and
rewrites the name tree as one sorted leaf. A rename that would collide with an existing name is
refused. `PdfOutline.DestinationName` gives the name a bookmark points at.
```csharp
PdfDocument doc = PdfDocument.Open("guide.pdf");
foreach (string name in doc.NamedDestinations.Names)
{
Console.WriteLine($"{name}: page {doc.NamedDestinations.GetPage(name)?.Index + 1}");
}
foreach (PdfOutline bookmark in doc.Outlines)
{
Console.WriteLine($"Bookmark {bookmark.Title} points at {bookmark.DestinationName}");
}
// Every bookmark, link and GoTo action that uses the old names follows the rename.
int changed = doc.NamedDestinations.Rename(new Dictionary
{
["_TOC_d1e10"] = "introduction",
["_TOC_d1e20"] = "installation",
});
Console.WriteLine($"{changed} references renamed");
doc.Save("guide-renamed.pdf");
```
## Save options
`Save` writes a new single revision file. By default it runs the lossless
[optimizer](https://www.jadev-corp.com/guides/compress-pdf/) on the result, with verification, because merged documents
often share fonts and images. `Save(path)` writes through a temporary file and replaces the
target atomically.
```csharp
doc.Save("manual-fast.pdf", new PdfSaveOptions
{
Optimize = false, // skip the optimizer pass, save sooner
Compression = CompressionEffort.Fast, // zlib level 1 for the streams the writer compresses
});
byte[] bytes = doc.ToArray(); // in memory, with the default options
using (var stream = new MemoryStream())
{
doc.Save(stream); // or to any writable stream
}
```
| `PdfSaveOptions` | Default | Notes |
|---|---|---|
| `Optimize` | true | Runs the lossless optimizer, with verification, on the saved file |
| `Compression` | `Maximum` | Flate effort: `Fast` (zlib level 1), `Balanced` (level 6) or `Maximum` (level 9) |
| `UseObjectStreams` | true | Packs objects into compressed object streams (PDF 1.5). Off writes a classic xref table |
| `Conformance` | `None` | Since 1.1: PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e or 4f, see [PDF/A](https://www.jadev-corp.com/guides/pdf-a/) |
## Limits
- Saving in full with `Save` writes a single revision: incremental updates of the source are not
kept, and the signatures of a signed document become invalid. `SaveIncremental` (1.1) appends
the changes instead and keeps them valid, see [Large files](https://www.jadev-corp.com/guides/large-files/). To add a
signature without breaking earlier ones, see [Signatures](https://www.jadev-corp.com/guides/signatures/).
- A document opened with only the user password of an encrypted file can be read but not saved.
- `Open` reads the whole file into memory. `OpenOnDemand` (1.1) reads objects as they are used,
see [Large files](https://www.jadev-corp.com/guides/large-files/) and
[Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/).
## Related
- [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/): what the save time optimizer does
- [Encryption](https://www.jadev-corp.com/guides/encryption/): open protected files and protect your output
- [Command line](https://www.jadev-corp.com/cli/): `merge`, `split`, `extract`, `rotate` and `meta`
- API: [PdfDocument](https://www.jadev-corp.com/api/pdfnetstack/pdfdocument/), [PdfPageCollection](https://www.jadev-corp.com/api/pdfnetstack/pdfpagecollection/), [PdfPage](https://www.jadev-corp.com/api/pdfnetstack/pdfpage/), [PdfDocumentInfo](https://www.jadev-corp.com/api/pdfnetstack/pdfdocumentinfo/), [PdfOutline](https://www.jadev-corp.com/api/pdfnetstack/pdfoutline/), [PdfNamedDestinations](https://www.jadev-corp.com/api/pdfnetstack/pdfnameddestinations/), [PdfSaveOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfsaveoptions/)
## Does merging PDFs duplicate shared fonts and images?
No. Resources shared by several imported pages are copied once, and saving runs the optimizer, which merges byte identical fonts and images across the merged documents.
## What happens to links and bookmarks when pages are removed?
Links and bookmarks that point to a removed page become empty. Links between pages imported in the same call keep working.
## Can PdfNetStack edit a signed PDF?
Yes. Saving in full writes a new single revision file and invalidates the signatures. From version 1.1, SaveIncremental appends the changes after the signed bytes instead, so every signature stays intact.
Source: https://www.jadev-corp.com/pdfnetstack/guides/edit-pages/
---
# Work with large PDF files in C#
> Open large PDFs in .NET without loading them into memory, read pages on demand, and append incremental updates that keep signatures valid. MIT licensed.
Available since 1.1.
Read a page of a large file without loading the rest, and save a change by appending it.
## Short answer
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.
## Opening on demand
`OpenOnDemand(stream, password, maxDecodedStreamBytes)` takes any readable stream. Memory use
follows what your code touches, not the size of the file: objects are parsed when first used,
and stream data is read from the stream each time it is needed and not kept. Offsets are read as
64 bit values.
A seekable stream belongs to the document while you use it:
- It must stay open and unchanged, and nothing else may read from it meanwhile.
- The document never disposes it: dispose it yourself when you are done.
- Open it with `FileAccess.ReadWrite` when you want `SaveIncremental` to append in place.
`PdfDocument` is disposable. Disposing a document read on demand releases what it reads from and
ends its use: anything that needs its objects afterwards throws `ObjectDisposedException`.
Disposing a document read into memory does nothing.
## Load options
`PdfLoadOptions` gathers how a document is opened, for `Open(path, options)`,
`Open(stream, options)`, `Open(memory, options)` and `OpenAsync`:
| Option | Default | What it does |
|---|---|---|
| `Password` | null | User or owner password; null tries the empty user password |
| `Mode` | `InMemory` | `OnDemand` parses objects when used and reads stream data when needed |
| `MaxDecodedStreamBytes` | 256 MiB | Ceiling for any single decoded stream, and for the bytes of one decoded image |
| `MaxImagePixels` | 2^28 | Most pixels one image may decode to; a larger one raises `PdfFormatException` before its pixels are allocated |
| `SpoolThreshold` | 16 MiB | How much of a stream that cannot seek is copied to memory before it goes to a file |
| `TempDirectory` | system temp folder | Where that file goes |
```csharp
var options = new PdfLoadOptions
{
Mode = PdfLoadMode.OnDemand, // parse objects when used, read stream data when needed
MaxDecodedStreamBytes = 64 << 20, // no single stream decodes to more than 64 MiB
MaxImagePixels = 50_000_000, // larger images are refused before they are decoded
};
using (PdfDocument doc = PdfDocument.Open("archive.pdf", options)) // the file stays open until Dispose
{
int characters = 0;
for (int i = 0; i < doc.Pages.Count; i++)
{
characters += doc.Pages[i].ExtractText().Length;
if (i % 50 == 49) doc.ReleaseUnusedObjects(); // frees what earlier pages parsed; edits are kept
}
Console.WriteLine($"{characters:N0} characters read on demand");
}
```
Opened on demand by path, the document opens the file itself, shared for reading, and keeps it
open until it is disposed. In memory, the file or stream is read once: a `MemoryStream` whose
buffer is exposed is used without copying, a seekable stream is read into an array of its length,
and any other stream in pieces joined once.
`MaxImagePixels` holds for every encoding a document's images use: raw samples (with PNG or TIFF
predictors), JPEG, JPEG 2000, JBIG2 and CCITT fax, by the size the dictionary gives and by the size
the data declares. For image files you place on pages, `PdfImage.Load(data, page, maxPixels)`
does the same for JPEG, PNG, GIF, BMP, TIFF and WebP and raises `FormatException`.
## Streams that cannot seek, and async reads
An HTTP request body or a storage download can be read once, front to back. Opened on demand,
such a stream is copied first: to memory up to `SpoolThreshold`, and past it to a temporary file
in `TempDirectory`. The copy belongs to the document and is deleted when the document is disposed,
or at once if opening fails. On Windows the file is opened with `DeleteOnClose`; elsewhere its
name is removed as soon as it is open, so nothing stays on disk if the process ends first. On AWS
Lambda, the one writable folder is `/tmp`.
`OpenAsync(stream, options, cancellationToken)` reads the stream with asynchronous reads only,
for hosts that forbid synchronous I/O, such as ASP.NET Core request bodies. On demand it always
reads from its own copy, so the stream is not used after the call. Cancelling stops the reads and
deletes any partial copy.
```csharp
// Stands in for a request body or a storage download: a stream read once, front to back.
await using (FileStream body = File.OpenRead("manual.pdf"))
{
var spooled = new PdfLoadOptions
{
Mode = PdfLoadMode.OnDemand,
SpoolThreshold = 8 << 20, // up to 8 MiB in memory, past it a temporary file
TempDirectory = Path.GetTempPath(), // "/tmp" on AWS Lambda
};
using PdfDocument doc = await PdfDocument.OpenAsync(body, spooled); // asynchronous reads only
Console.WriteLine($"{doc.Pages.Count} pages; the copy is deleted when the document is disposed");
}
```
## Memory while reading page by page
A document read on demand keeps every object it parses: on pages that each have their own font,
about 25 KB a page. `ReleaseUnusedObjects` frees the parsed objects nothing uses any more, and
they are parsed again if you need them later. It cannot lose an edit: an object that differs from
the file is kept, and so is every object still referenced from anywhere, which stays the instance
the document returns. To find those, it runs a full garbage collection, so call it every few dozen
pages rather than after each one.
On a synthetic document of 300 pages, each with its own font, descriptor, resources and link,
reading the text of every page kept 1,503 parsed objects and 7.8 MB. Releasing every 50 pages
kept 301 objects (the pages themselves) and 1.0 MB.
Everything else works as on a document opened with `Open`: pages, text and image extraction,
annotations, forms, metadata and the object model. A full save with `Save` or `ToArray` still
builds the whole output in memory.
## Incremental updates
`SaveIncremental(output)` writes an incremental update (ISO 32000-1, 7.5.6): the original file
byte for byte, then the objects that changed or were added, and a cross-reference section that
points back to the original's.
| Output | What is written |
|---|---|
| The stream given to `OpenOnDemand` | Only the update, appended at the end. Later saves build on the updated file |
| Any other stream | The original bytes, then the update |
Because the original bytes stay as they were, signatures over them stay valid. Here a note is added
to a signed contract:
```csharp
using (var input = File.OpenRead("contract-signed.pdf"))
using (var output = File.Create("contract-noted.pdf"))
{
using PdfDocument doc = PdfDocument.OpenOnDemand(input);
doc.Pages[0].Annotations.AddNote(520, 40, "Filed on 29 October", author: "Records");
doc.SaveIncremental(output); // the signed bytes, then the update
}
foreach (PdfSignatureInfo s in PdfSigner.Verify(File.ReadAllBytes("contract-noted.pdf")))
{
Console.WriteLine($"{s.FieldName}: intact {s.IsIntact}, whole file {s.CoversWholeDocument}");
}
```
The signature is still intact. It no longer covers the whole file, because the note came after
it.
`SaveIncremental` refuses, with `NotSupportedException`, a document that is encrypted or set to be
encrypted, and one that was damaged and had to be rebuilt when opened: save those in full with
`Save`. A document created with `PdfDocument.Create` has no original to append to, and raises
`InvalidOperationException`.
## Saving large files
A full save without the optimizer is written progressively: each object, or each batch of
objects packed into an object stream, goes to the output as soon as it is serialized, offsets
are counted as the bytes go out, and the cross-reference data comes last. Memory beyond the
document follows the largest object, not the file, and for a document opened on demand the
stream data is copied from the source as it is written. The bytes are the ones `ToArray`
returns for the same options.
`SaveAsync` writes the same file with asynchronous calls only and flushes the stream at the end,
so it can write straight into an ASP.NET Core or Azure Functions response body, which refuse
synchronous writes. `HtmlConverter.ConvertToPdfAsync` does the same for HTML.
```csharp
using (var source = File.OpenRead("manual.pdf"))
using (var output = File.Create("manual-rotated.pdf"))
{
PdfDocument doc = PdfDocument.OpenOnDemand(source);
doc.Pages[0].Rotate(90);
await doc.SaveAsync(output, new PdfSaveOptions { Optimize = false }); // object by object, asynchronous writes
}
Console.WriteLine($"{new FileInfo("manual-rotated.pdf").Length:N0} bytes written progressively");
```
| Save | Memory beyond the document |
|---|---|
| `Optimize = false` | The largest object or object stream batch, and a few bytes of cross-reference data per object |
| `Optimize = true`, the default | The whole file, twice: the optimizer reads all of it and proves its rewrite before any of it is used |
| Encrypted, with the optimizer | As above; encryption is a last pass over the proven file |
Cancelling `SaveAsync` stops it before the next object; what reached the stream is then not a
complete file, and `SaveAsync(path)` leaves the file as it was. A failed write surfaces as the
stream's own exception.
To shrink a file too large for memory, save it without the optimizer, then run the optimizer in
streaming mode. `PdfOptimizerOptions.Streaming` reads the source on demand, recompresses streams a
batch of at most 32 MiB of source data at a time, and writes the rewrite to a temporary file: the
destination's own for `OptimizeFileAsync`, otherwise one in the system temporary folder, which
needs room for the output. The proof then reads that file back on demand and compares it with
the source exactly as in memory, every reachable object and every stream by its decoded content.
Only a proven rewrite, or else the input, reaches the destination.
```csharp
var options = new PdfOptimizerOptions { Streaming = true, MaxInputBytes = 16L << 30 };
PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("manual-rotated.pdf", "manual-small.pdf", options);
Console.WriteLine($"{result.Disposition}, streamed {result.Statistics.Streamed}: {result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes");
Console.WriteLine(result.Reason);
```
On a generated 278 MB file of 400 image pages (`tools/PdfNetStack.MemoryBench`), the streaming
optimizer peaked at about 50 MB of live managed memory against 810 MB in memory, took no longer,
and wrote the same bytes. The same file saved progressively from a document opened on demand
peaked at 3 MB instead of 835 MB.
## Compressing large files
By default the optimizer reads the whole document into memory. Plan for about 10 times the file
size, more for image heavy files, and lower `MaxDegreeOfParallelism` to cap memory, or use
`Streaming` as above. Inputs larger than `MaxInputBytes` (1 GiB by default) are returned
unchanged; raise it for streaming mode. See
[Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/).
## Limits
- `ToArray` builds the whole output in memory, and so does a full `Save` with the optimizer on.
- In memory, the optimizer holds the whole document, whichever way the file was opened.
`Streaming` cannot be combined with `Linearize`, and its output can differ slightly from the
in-memory rewrite: duplicates are found before streams are recompressed, so two JPEG files
that differ only in metadata stay two.
- Read into memory, a file is limited to 2 GiB, and a stream that cannot seek takes up to about
twice its size while it is read. Open larger files on demand.
- Decoded object streams are kept apart from parsed objects, up to `MaxDecodedStreamBytes` in
total, and `ReleaseUnusedObjects` does not free them.
- A temporary copy of a stream is never written to: `SaveIncremental` given another stream writes
the original bytes and the update there.
- Two saves of an encrypted document never match byte for byte (each string and stream gets a
random initialization vector), nor do two PDF/A saves (each records a new instance id and the
time of the save).
- An incremental update keeps everything the original holds: it never makes a file smaller. Run
the optimizer for that, on a document that is not signed.
## Related
- [Edit pages](https://www.jadev-corp.com/guides/edit-pages/): opening, editing and saving in full
- [Signatures](https://www.jadev-corp.com/guides/signatures/): signing appends an incremental update too
- [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/)
- [Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/)
- [Safety model](https://www.jadev-corp.com/concepts/safety-model/): the limits that bound hostile input
- API: [PdfDocument](https://www.jadev-corp.com/api/pdfnetstack/pdfdocument/), [PdfLoadOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfloadoptions/)
## Can PdfNetStack open a PDF without reading the whole file into memory?
Yes, from version 1.1. PdfDocument.OpenOnDemand reads the cross-reference data and the page tree, and parses every other object when it is first used. Stream data such as page content, images and fonts is read from the stream each time it is needed and not kept.
## Can PdfNetStack read a PDF from an HTTP request or a storage download in AWS Lambda or Azure Functions?
Yes, from version 1.1. PdfDocument.OpenAsync reads the stream with asynchronous reads only. Opened on demand, a stream that cannot seek is copied to memory up to PdfLoadOptions.SpoolThreshold and past it to a temporary file in TempDirectory, such as /tmp on Lambda, deleted when the document is disposed.
## How do I limit the memory one PDF can take?
PdfLoadOptions caps any decoded stream (MaxDecodedStreamBytes, 256 MiB by default) and the pixels of any decoded image (MaxImagePixels, 2^28 by default). Open large files on demand, and call ReleaseUnusedObjects while reading many pages.
## Does saving an edited signed PDF break its signatures?
Not with SaveIncremental. It writes the original bytes unchanged followed by the changed objects, so every signature over the original stays intact. A full save rewrites the file and invalidates them.
Source: https://www.jadev-corp.com/pdfnetstack/guides/large-files/
---
# Draw on PDF pages in C# and .NET
> Draw shapes, paths, text, gradients and transparency on new or existing PDF pages in C# with a top left canvas in points. Pure .NET, MIT licensed.
Available since 1.1.
A canvas for PDF pages: shapes, paths, colors, gradients, transforms and clipping, on new pages and
on pages you opened.
## Short answer
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");
```

## The canvas
```csharp
PdfDocument doc = PdfDocument.Create();
PdfPage page = doc.Pages.Add(PdfRectangle.A4); // 595 x 842 points
using PdfGraphics g = page.CreateGraphics(); // origin top left, y down, points
```
`g.Width` and `g.Height` give the page size in points. A4 is 595 by 842 points, US Letter 612 by
792. The page's `/Rotate` is not applied: coordinates are those of the unrotated page. See
[Coordinates and units](https://www.jadev-corp.com/concepts/coordinates-and-units/) for how this canvas relates to the
coordinates text and image extraction report.
A `PdfGraphics` is not thread safe. Use one canvas per page and per thread.
## Shapes and paths
Every shape method takes a pen for the outline and a brush for the fill, in that order. Pass
`null` for either one to leave it out. A `PdfColor` converts to a solid brush.
```csharp
// Pen first (or null for no outline), then brush (or null for no fill).
g.DrawRectangle(null, PdfColor.FromHex("#1E3A8A"), 0, 0, g.Width, 90);
g.DrawRoundedRectangle(new PdfPen(PdfColor.FromHex("#CBD5E1")), PdfColor.White, 40, 120, 250, 150, 12);
g.DrawEllipse(null, PdfColor.FromHex("#14B8A6"), 70, 150, 90, 90);
g.DrawLine(new PdfPen(PdfColor.Black, 2) { DashPattern = [6, 3], LineCap = PdfLineCap.Round }, 40, 300, 290, 300);
var path = new PdfPath()
.MoveTo(180, 240)
.CurveTo(200, 150, 250, 150, 270, 240)
.Close();
g.DrawPath(new PdfPen(PdfColor.FromHex("#7C3AED"), 3) { LineJoin = PdfLineJoin.Round }, null, path);
```
| Method | Draws |
|---|---|
| `DrawLine`, `DrawLines` | A segment, or connected segments through a list of points |
| `DrawRectangle` | A rectangle from its top left corner |
| `DrawRoundedRectangle` | A rectangle with one radius, or with `PdfCornerRadii` for elliptical corners sized like CSS `border-radius` |
| `DrawEllipse` | An ellipse inscribed in a rectangle |
| `DrawArc`, `DrawPie` | Part of an ellipse; angles in degrees from the x axis, clockwise on the canvas |
| `DrawBezier` | A cubic Bezier curve |
| `DrawPolygon` | A closed polygon, with the nonzero or even-odd fill rule |
| `DrawPath` | Any `PdfPath` |
`PdfPath` builds outlines step by step: `MoveTo`, `LineTo`, `CurveTo`, `QuadraticCurveTo`,
`AddArc`, `AddRectangle`, `AddRoundedRectangle`, `AddEllipse`, `AddCircle`, `AddPolygon` and
`Close`. Its `FillRule` decides how overlapping parts fill.
### Pens
`PdfPen` is an immutable record: create one with a color and a width, and derive variants with
`with`.
| Property | Default | Notes |
|---|---|---|
| `Color` | | Stroke color, with its opacity |
| `Width` | 1 | In points. 0 draws the thinnest line the device can show |
| `LineCap` | `Butt` | `Butt`, `Round` or `Square` |
| `LineJoin` | `Miter` | `Miter`, `Round` or `Bevel` |
| `MiterLimit` | 10 | At least 1 |
| `DashPattern` | solid | Alternating dash and gap lengths in points |
| `DashOffset` | 0 | Where in the pattern the line starts |
## Colors, gradients and transparency
```csharp
var gradient = new PdfLinearGradientBrush(new PdfPoint(310, 0), new PdfPoint(555, 0),
PdfColor.FromHex("#2563EB"), PdfColor.FromHex("#DB2777"));
g.DrawRoundedRectangle(null, gradient, 310, 120, 245, 70, 12);
var glow = new PdfRadialGradientBrush(new PdfPoint(370, 245), 50, PdfColor.White, PdfColor.FromHex("#F59E0B"));
g.DrawEllipse(null, glow, 320, 195, 100, 100);
g.DrawEllipse(null, PdfColor.Cmyk(0, 0.9, 0.85, 0), 440, 200, 80, 80); // CMYK, for print
using (g.Save())
{
g.BlendMode = PdfBlendMode.Multiply; // one of the 16 PDF blend modes
g.DrawEllipse(null, PdfColor.Rgb(0.1, 0.6, 0.9, alpha: 0.8), 480, 220, 60, 60); // 80% opaque
}
```
- `PdfColor.Gray`, `Rgb` (components 0 to 1), `FromRgb` (0 to 255), `Cmyk` and `FromHex` (`#RGB`,
`#RGBA`, `#RRGGBB` or `#RRGGBBAA`) create device colors. CMYK is written as DeviceCMYK, for
print workflows.
- Every color has an opacity: pass `alpha`, or call `WithAlpha`. Opacity below 1 is written as a
constant alpha graphics state.
- `PdfLinearGradientBrush` and `PdfRadialGradientBrush` take two colors, or a list of
`PdfGradientStop` values for more. Gradient points are canvas coordinates.
- `BlendMode` sets one of the 16 PDF blend modes, such as `Multiply` or `Screen`, for what is
drawn next. It is saved and restored with the graphics state.

## Transforms, saved states and clipping
```csharp
using (g.Save()) // transform, clip and colors come back after the block
{
g.TranslateTransform(165, 400);
for (int i = 0; i < 12; i++)
{
g.RotateTransform(30);
g.DrawRectangle(null, PdfColor.FromHex("#0EA5E9").WithAlpha(0.35), 0, -6, 90, 12);
}
}
using (g.Save())
{
g.IntersectClip(new PdfPath().AddCircle(430, 400, 70)); // nothing outside the circle shows
for (double x = 300; x < 540; x += 14)
{
g.DrawLine(new PdfPen(PdfColor.FromHex("#1E3A8A"), 6), x, 320, x + 60, 480);
}
}
```
`TranslateTransform`, `ScaleTransform`, `RotateTransform` (clockwise for positive angles),
`RotateAtTransform` and `MultiplyTransform` with a `PdfMatrix` change where later drawing lands.
`Save` records the transform, clip, colors and pen settings; dispose its result, or call
`Restore`, to return to them. `IntersectClip` narrows the area that shows to a rectangle or a path.
## Drawing behind existing content
`CreateGraphics(PdfGraphicsLayer.Background)` draws underneath what a page already shows. Use it
for letterheads, stationery and colored backgrounds that must not cover the text.
```csharp
PdfDocument doc = PdfDocument.Open("report.pdf");
foreach (PdfPage page in doc.Pages)
{
// Background: drawn underneath the page's existing content, which stays on top.
using PdfGraphics g = page.CreateGraphics(PdfGraphicsLayer.Background);
g.DrawRectangle(null, PdfColor.FromHex("#E0F2FE"), 0, 0, g.Width, 160);
g.DrawLine(new PdfPen(PdfColor.FromHex("#0284C7"), 2), 40, 160, g.Width - 40, 160);
g.DrawString("Example Company, 1 Main Street", PdfStandardFont.Helvetica, 9, PdfColor.FromHex("#0369A1"), 400, 24);
}
doc.Save("report-letterhead.pdf");
```

On an existing page, the page's own content is wrapped in a save and restore pair, so graphics
state it leaves behind cannot change your drawing. Resources shared with other pages are never
modified.
## Text, images and links on the canvas
The canvas also draws text and images. Each has its own guide:
- `DrawString` and `DrawText` for single lines and wrapped paragraphs: [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/).
- `DrawImage`, `DrawSvg` and `DrawForm` for images, vector drawings and pages of other PDFs:
[Images](https://www.jadev-corp.com/guides/images/).
- `AddLink` for a clickable area that follows the current transform: [Annotations](https://www.jadev-corp.com/guides/annotations/).
- `DrawInvisibleText` writes text that is not drawn but selects, copies and searches, stretched to
fill a box. Laid over the words an OCR engine found, it makes a scanned page searchable. See
[OCR](https://www.jadev-corp.com/guides/ocr/).
## Limits
- Colors are device colors. There is no ICC based color management for drawn colors, and
`ToRgb` converts CMYK by the naive formula.
- Patterns and shadings beyond linear and radial gradients are not available on the canvas.
## Related
- [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/)
- [Images](https://www.jadev-corp.com/guides/images/)
- [Coordinates and units](https://www.jadev-corp.com/concepts/coordinates-and-units/)
- API: [PdfGraphics](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfgraphics/), [PdfPath](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfpath/), [PdfPen](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfpen/), [PdfColor](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfcolor/), [PdfLinearGradientBrush](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdflineargradientbrush/), [PdfMatrix](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfmatrix/)
## Can I draw on a page of an existing PDF?
Yes. PdfPage.CreateGraphics works on new and opened pages. It draws over the existing content by default, or underneath it with PdfGraphicsLayer.Background.
## Which color models are supported?
Gray, RGB and CMYK device colors, each with an opacity, plus linear and radial gradients and the 16 PDF blend modes.
Source: https://www.jadev-corp.com/pdfnetstack/guides/drawing/
---
# Fonts and text in PDF with C# and .NET
> Draw text in PDFs from C# with standard fonts or embedded, subset OpenType, WOFF2 and variable fonts, wrapped paragraphs, Arabic and Indic. MIT licensed.
Available since 1.1.
Text on PDF pages: the standard fonts, embedded TrueType and OpenType fonts, wrapped and justified
paragraphs, and right to left scripts.
## Short answer
`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);
```

## One line of text
`DrawString(text, font, size, brush, x, y)` places the start of the baseline at (x, y). To place
the top of the line instead, add `font.Ascent * size` to y. Another overload takes a pen and a
brush, so glyphs can be filled with a gradient, outlined, or both.
`MeasureString(text, size)` returns the width in points, which is all you need to right align or
center a line. Font metrics are fractions of the font size: multiply by the size to get points.
| Member | Meaning |
|---|---|
| `Ascent`, `Descent` | Height above and depth below the baseline used for line spacing (descent is positive) |
| `LineGap`, `GetLineHeight(size)` | Recommended extra space, and the default line height in points |
| `CapHeight`, `XHeight` | Height of flat capitals and of flat lowercase letters |
| `UnderlinePosition`, `UnderlineThickness` | Where an underline sits |
| `StrikeoutPosition`, `StrikeoutThickness` | Where a strikethrough sits |
| `HasGlyph(codePoint)`, `CanRender(text)` | Whether the font can draw a character or a string |
## The 14 standard fonts
`PdfStandardFont` has Helvetica, Times and Courier with their bold and italic styles, Symbol and
ZapfDingbats. Viewers supply them, so nothing is embedded.
- Latin text uses WinAnsiEncoding (Windows-1252). Up to 37 further characters per font and
document, such as Central European letters and the minus sign, get codes through a
/Differences array.
- Symbol and ZapfDingbats take their own Unicode characters.
- A character the font cannot draw throws an `ArgumentException`. For other scripts, embed a font.
## Embedded fonts
```csharp
// Any TrueType or OpenType font: .ttf, .otf, .ttc, .woff or .woff2. Only the glyphs drawn are embedded.
PdfFont inter = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2");
g.DrawString("Inter, embedded as a subset, kerned and searchable.", inter, 14, PdfColor.FromHex("#1E3A8A"), 40, 100);
```
- TrueType and OpenType (CFF) fonts load from .ttf, .otf, .ttc and .otc collections, WOFF and
WOFF2. For a collection, pass the index of the font; `CountFonts` says how many it holds.
- Drawn text embeds a subset of the glyphs used, as a Type 0 font with a ToUnicode map, so the
text stays searchable and copyable.
- Pairs are kerned from the font's GPOS or kern table.
- A font is written into a document the first time text is drawn with it, once per document. One
`PdfFont` instance serves every size and can be shared between documents and threads.
- A font whose license forbids embedding (the OS/2 fsType flags) is refused when first drawn. A
damaged file raises `FontFormatException`.
## Variable fonts
A variable font holds a range of styles along design axes such as weight (`wght`) or width
(`wdth`). Drawn as loaded, it shows its default instance. `IsVariable`, `VariationAxes` and
`NamedInstances` describe it:
```csharp
var font = PdfOpenTypeFont.Load("fonts/Variable.ttf");
Console.WriteLine($"Variable: {font.IsVariable}");
foreach (PdfFontAxis axis in font.VariationAxes)
{
Console.WriteLine($"{axis.Tag} ({axis.Name}): {axis.Minimum} to {axis.Maximum}, default {axis.Default}");
}
Console.WriteLine($"Named instances: {string.Join(", ", font.NamedInstances)}");
```
`GetInstance(name)` gives a named instance, such as "Bold", and `GetInstance(axisValues)` any
point in the design space. Axes left out keep their default, and values outside an axis's range
are clamped to it. Each instance is a `PdfOpenTypeFont` embedded as a static font, subset like any
other.
```csharp
PdfOpenTypeFont thin = font.GetInstance("Thin");
PdfOpenTypeFont bold = font.GetInstance("Bold");
PdfOpenTypeFont custom = font.GetInstance(new Dictionary { ["wght"] = 550 });
g.DrawString("HOT TO", thin, 40, PdfColor.Black, 40, 70);
g.DrawString("HOT TO", font, 40, PdfColor.Black, 40, 130); // the default instance
g.DrawString("HOT TO", custom, 40, PdfColor.Black, 40, 190);
g.DrawString("HOT TO", bold, 40, PdfColor.Black, 40, 250);
```

- TrueType outlines move by their gvar deltas, advances come from HVAR, font metrics from MVAR, and
control values from cvar, through the avar mapping. The weight and width classes and the italic
angle follow the axes.
- CFF2 fonts load at their default instance and are converted to CID keyed CFF for embedding;
instances apply their blends.
- Shaping applies the variation deltas of kerning and mark positions, and GSUB and GPOS feature
variations, so an instance can swap glyphs.
- Outlines, advances and metrics match the fontTools instancer, and shaping matches HarfBuzz, on
the synthetic fixtures and variable fonts the test suite compares. Damaged variation data raises
`FontFormatException`.
## Font collections
`PdfFontCollection` finds fonts by family, weight and style the way CSS matches them. Adding a file
or a directory reads only the font's names and style; the font itself loads when first found.
`PdfFontCollection.System` holds the fonts installed on the machine.
```csharp
var fonts = new PdfFontCollection();
fonts.AddDirectory("fonts"); // reads names and styles only
PdfFont? found = fonts.Find("Inter", weight: 700); // null when the family is unknown
PdfFont interBold = found ?? PdfStandardFont.HelveticaBold;
PdfFont arabic = fonts.Find("Noto Sans Arabic") ?? throw new InvalidOperationException("Font missing");
```
`Find(family, weight, italic)` takes a CSS weight from 1 to 1000 (400 regular, 700 bold) and
returns the closest face, or null when the family is unknown. The collection is thread safe.
## Paragraphs
`PdfParagraph` holds rich text: spans, each with its own `PdfTextStyle`. `DrawText` lays the
paragraph out at a width, wraps and aligns its lines, and draws it with its top left corner at
(x, y). It returns the `PdfTextLayout`, whose `Height` tells how much room it took.
```csharp
var body = new PdfTextStyle(inter, 11);
var paragraph = new PdfParagraph { Alignment = PdfTextAlignment.Justify, LineHeight = 1.5 }
.Add("Payment is due within ", body)
.Add("30 days", body with { Font = interBold })
.Add(" of the invoice date. Late payments carry interest at the legal rate, and reminders are sent ", body)
.Add("by email", body with { Underline = true, Brush = PdfColor.FromHex("#2563EB") })
.Add(". Thank you for your business.", body);
PdfTextLayout layout = g.DrawText(paragraph, 40, 130, 300); // top left corner and width
```
| `PdfParagraph` | Default | Notes |
|---|---|---|
| `Alignment` | `Start` | `Start`, `End`, `Left`, `Right`, `Center` or `Justify` (the last line aligns to the start) |
| `Direction` | `Auto` | From the first strong character, or `LeftToRight`, `RightToLeft` |
| `LineHeight` | null | A multiple of each span's font size, as CSS `line-height` takes a number; null uses the font's own spacing |
| `FirstLineIndent` | 0 | In points |
| `PdfTextStyle` | Default | Notes |
|---|---|---|
| `Font`, `Size` | | Required |
| `Brush` | black | Fill of the glyphs and decorations; null for outlines only |
| `Pen` | null | Outline of the glyphs |
| `Fallbacks` | none | Fonts tried in order for characters `Font` lacks |
| `LetterSpacing`, `WordSpacing` | 0 | Extra space in points |
| `Underline`, `Strikethrough` | false | Decorations |
`PdfTextStyle` is a record, so `body with { Font = interBold }` derives a variant.
Lines break at Unicode line break opportunities (UAX #14). A word wider than the line breaks
between characters. Line feeds start a new line, tabs advance four spaces, and soft hyphens
(U+00AD) show a hyphen when a word breaks there.
## Hyphenation
Hyphenation is off by default. Give a paragraph a `PdfHyphenator` and words break at the line's end
where the language allows, with a hyphen. The core library has Liang's algorithm, as TeX defines
it, and no patterns; the optional `PdfNetStack.Hyphenation` package has them.
```csharp
// Opt in per paragraph. A language loads on first use; the hyphenator is shared and thread safe.
var german = new PdfParagraph { Alignment = PdfTextAlignment.Justify, Hyphenator = PdfHyphenationPatterns.ForLanguage("de") }
.Add("Die Rechtschreibung regelt die Silbentrennung, auch im Bundesverfassungsgericht.", new PdfTextStyle(PdfStandardFont.Helvetica, 11));
g.DrawText(german, 50, 60, 160);
```
| Tag | Language | Tag | Language |
|---|---|---|---|
| `en-us` (also `en`, `en-CA`) | English, American | `en-gb` (also `en-AU`, `en-NZ`, `en-IE`, `en-ZA`, `en-IN`) | English, British |
| `de` (also `de-AT`, `de-CH`) | German, reformed spelling | `fr` | French |
| `nl` | Dutch | `es` | Spanish |
| `it` | Italian | `pt` (also `pt-BR`, `pt-PT`) | Portuguese |
| `af` | Afrikaans | `be` | Belarusian |
| `bg` | Bulgarian | `cy` | Welsh |
| `da` | Danish | `el` (`el-monoton`), `el-polyton` | Greek |
| `et` | Estonian | `eu` | Basque |
| `ga` | Irish | `gl` | Galician |
| `hr` | Croatian | `la` | Latin |
| `lt` | Lithuanian | `nb` (also `no`), `nn` | Norwegian |
| `pl` | Polish | `sk` | Slovak |
| `sl` | Slovene | `sq` | Albanian |
| `uk` | Ukrainian | | |
Patterns come from TeX's [hyph-utf8](https://github.com/hyphenation/tex-hyphen). Only sets with a
permissive license ship, so some languages have no patterns: `ca`, `cs`, `eo`, `fi`, `hu`, `hy`, `id`,
`is`, `lv`, `mk`, `ro`, `ru`, `sr`, `sv`, `th` and `tr`, with German in the 1901 spelling
(`de-1901`), which has its own breaks. `ForLanguage` returns null for those. If you are licensed to
use other patterns, `PdfHyphenator.Create` takes them in TeX's format.
Each set brings the minimum letters before and after a hyphen its source gives. `MinWordLength` is 5
by default. Words the patterns get wrong, or that must never break, go in as exceptions:
```csharp
// Tune the limits, and add your own words: a word without a hyphen never breaks.
PdfHyphenator english = PdfHyphenationPatterns.ForLanguage("en-GB")!.WithExceptions(["PdfNetStack", "ta-ble"]) with { MinWordLength = 6, MinLeft = 3 };
string marked = english.InsertSoftHyphens("Characterisation of programmes"); // soft hyphens at each point
Console.WriteLine(string.Join("|", english.FindBreaks("Characterisation")));
```
Left alone: words with a soft hyphen, whose breaks are the author's; words joined to a digit, or
holding a capital past the first letter; words of all capitals, unless `HyphenateAllCaps`; web and
e-mail addresses and paths; words over 64 letters. Results do not depend on the current culture.
`PdfParagraph.Text` keeps words whole, and extracted text marks a shown hyphen with U+00AD. In HTML,
see [`hyphens: auto`](https://www.jadev-corp.com/guides/html-to-pdf/css-support/#hyphenation).
## Right to left and Arabic
Text runs through the Unicode bidirectional algorithm (UAX #9) and an OpenType shaper: Arabic and
Persian letters join and take their contextual forms, ligatures form, vowel marks sit on their
letters, and mixed right to left and left to right text is ordered with mirrored brackets. The
line breaking and bidi implementations pass the Unicode 16 conformance suites, and the shaper
matches HarfBuzz on the fonts the test suite compares.
```csharp
var arabicStyle = new PdfTextStyle(arabic, 16) { Fallbacks = [inter] }; // digits and € come from Inter
var rtl = new PdfParagraph { Direction = PdfTextDirection.RightToLeft }
.Add("هذا نص عربي يقرأ من اليمين إلى اليسار. المبلغ المستحق: 1,250.00 €", arabicStyle);
g.DrawText(rtl, 40, 140 + layout.Height + 20, 300); // start aligned: on the right
```
In a right to left paragraph, `Start` alignment means the right edge. Shaped and right to left
text carries ActualText, so it copies and searches in logical order.
### Kashida justification
With `Kashida = true` and `Justify` alignment, justified Arabic lines elongate joined letters
with tatweel before the spaces widen: one elongation per word, up to three tatweel widths, at the
point Arabic typography prefers. Tatweels carry no text, so copying gives the original words. A
font without a tatweel glyph justifies with spaces only. It is off by default.
```csharp
// Justified Arabic lines elongate one join per word with tatweel before spaces widen.
var justified = new PdfParagraph { Direction = PdfTextDirection.RightToLeft, Alignment = PdfTextAlignment.Justify, Kashida = true }
.Add("هذا نص عربي يضبط على الجانبين بالكشيدة، فتطول الحروف المتصلة قبل أن تتسع المسافات بين الكلمات.", arabicStyle);
g.DrawText(justified, 40, 420, 300);
```
## Indic scripts
Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada and Malayalam are shaped
per Microsoft's script development specifications: syllables are found, the base consonant, reph
and pre-base matras reorder, the font's basic and presentation forms apply in order, and broken
clusters get a dotted circle. Fonts built to the old (deva) and the new (dev2) specification both
work. The shaper matches HarfBuzz on a synthetic Devanagari and Bengali fixture and on a font
covering all nine scripts. Draw with a font that covers the script, through `DrawString`, `DrawText` or a fallback in `PdfTextStyle.Fallbacks`.
## Unicode normalization
The shaper normalizes text as HarfBuzz does. A character the font lacks is decomposed into ones it
has, marks are sorted into canonical order, and a base and a mark are composed when the font has the
composite glyph. Spaces and the non-breaking hyphen draw with the space and hyphen glyphs when the
font has no glyph of their own. `CanRender`, font fallback and HTML text use the same coverage
rules, so "é" typed as e plus a combining accent draws in a font that only has the precomposed
letter.
## Text that continues on the next page
`PdfTextLayout.Create` measures a paragraph without drawing it. Each line reports its `Top`,
`Baseline` and `Height`, so you can decide how many lines fit, then draw a range of lines with
`DrawText(layout, x, y, first, count)` and continue on the next page.
```csharp
var style = new PdfTextStyle(PdfStandardFont.TimesRoman, 12);
var paragraph = new PdfParagraph { Alignment = PdfTextAlignment.Justify }.Add(text, style);
PdfTextLayout layout = PdfTextLayout.Create(paragraph, 595 - 2 * 72); // measure before drawing
PdfDocument doc = PdfDocument.Create();
const double top = 72, bottom = 842 - 72;
int next = 0;
while (next < layout.Lines.Count)
{
// As many lines as fit between the margins, measured from the first line of this page.
double pageTop = layout.Lines[next].Top;
int count = 0;
while (next + count < layout.Lines.Count
&& layout.Lines[next + count].Top + layout.Lines[next + count].Height - pageTop <= bottom - top)
{
count++;
}
PdfPage page = doc.Pages.Add(PdfRectangle.A4);
using PdfGraphics g = page.CreateGraphics();
g.DrawText(layout, 72, top, next, count);
next += count;
}
doc.Save("flow.pdf");
Console.WriteLine($"{layout.Lines.Count} lines on {doc.Pages.Count} pages");
```
## Limits
- `PdfFontCollection` and HTML do not choose variable font instances yet: they draw a variable
font at its default instance. Use `GetInstance` for other styles.
- Southeast Asian scripts (the Universal Shaping Engine), Khmer, Myanmar, Thai and Hangul shapers
are on the [roadmap](https://www.jadev-corp.com/roadmap/). Lines are filled greedily.
- A hyphenation point inside a ligature is skipped, since breaking there would need the word shaped
again. Fonts without a hyphen glyph do not hyphenate. One hyphenator serves a whole paragraph;
for mixed languages, use `InsertSoftHyphens` span by span.
- Standard fonts cover Windows-1252 plus up to 37 further characters per font and document.
## Related
- [Drawing](https://www.jadev-corp.com/guides/drawing/)
- [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/): CSS layout with `@font-face` and web fonts
- [PDF/A](https://www.jadev-corp.com/guides/pdf-a/): archival output needs embedded fonts
- API: [PdfFont](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdffont/), [PdfStandardFont](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdfstandardfont/), [PdfOpenTypeFont](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdfopentypefont/), [PdfFontAxis](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdffontaxis/), [PdfFontCollection](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdffontcollection/), [PdfParagraph](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfparagraph/), [PdfTextStyle](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdftextstyle/), [PdfTextLayout](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdftextlayout/)
## Is text drawn by PdfNetStack searchable?
Yes. Embedded fonts get a ToUnicode map, so text selects, copies and searches in any script the font covers. Shaped and right to left text also carries ActualText, so it copies in logical order.
## Does embedding a font make the file large?
No. Only the glyphs you draw are embedded, as a subset. The 14 standard fonts are not embedded at all, so a page of text in them weighs about 2 KB.
## Can I write Arabic, Hebrew or Hindi?
Yes. Text runs through the Unicode bidirectional algorithm and an OpenType shaper with Arabic and Indic shaping, so Arabic letters join, Devanagari syllables reorder and mixed directions are ordered. Use a font that has the script's glyphs.
## Are variable fonts supported?
Yes. PdfOpenTypeFont.GetInstance gives a named instance or any axis values of a TrueType or CFF2 variable font, embedded as a static font.
Source: https://www.jadev-corp.com/pdfnetstack/guides/fonts-and-text/
---
# Add images to PDF in C# and .NET
> Place JPEG, PNG, GIF, BMP, TIFF, WebP and SVG images on PDF pages in C#, fit them to boxes, and reuse pages of other PDFs as watermarks. MIT licensed.
Available since 1.1.
Images, vector drawings and pages of other PDFs, placed on a page wherever you want them.
## Short answer
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
```

## Image formats
| Format | How it is embedded |
|---|---|
| JPEG | Copied without recompression. EXIF orientation, Adobe CMYK, ICC profiles and JFIF density are honored |
| PNG | All color types and bit depths, interlaced or not. Simple PNGs keep their compressed data; an alpha channel or tRNS transparency becomes a soft mask. ICC profiles and pixel density are kept |
| GIF | GIF 87a and 89a, the first frame, interlaced or not, with its transparent color as a soft mask |
| BMP | 1 to 32 bits per pixel, RLE4 and RLE8, bit fields, top down rows, alpha, or an embedded JPEG or PNG. Decoded and Flate compressed; palette images keep their palette |
| TIFF | Strips and tiles, chunky and planar; uncompressed, PackBits, LZW and Deflate with the predictor, CCITT modified Huffman, Group 3 and Group 4, JPEG; bilevel, gray, palette, RGB, CMYK and YCbCr samples of 1 to 16 bits, associated and unassociated alpha, BigTIFF. A page stored as one strip of fax or JPEG data is embedded as is; other pages are decoded and Flate compressed |
| WebP | Lossy and lossless, with alpha, the first frame of an animation. Lossy WebP decodes to the same pixels as libwebp |
| SVG | `PdfSvg`: drawn as PDF vectors, see below |
`PdfImage.Load` takes a path, a stream or a byte array, and reports its `Format`, `PixelWidth`,
`PixelHeight`, `HorizontalResolution`, `VerticalResolution` and `HasTransparency`. Alpha becomes a
soft mask. Decoding is bounded, and an unsupported or damaged file raises a `FormatException`.
```csharp
PdfImage webp = PdfImage.Load("photo.webp"); // lossy or lossless WebP, alpha as a soft mask
PdfImage bmp = PdfImage.Load("logo.bmp"); // BMP, 1 to 32 bits per pixel
PdfImage fax = PdfImage.Load("scan.tif", 1); // page 2 of a multi-page TIFF, from 0
Console.WriteLine($"{webp.Format} {webp.PixelWidth}x{webp.PixelHeight}, {bmp.Format}, {fax.Format}");
Console.WriteLine($"scan.tif has {PdfImage.GetPageCount("scan.tif")} pages");
```
A TIFF file can hold several pages. `PdfImage.GetPageCount` counts them and `PdfImage.Load(path,
page)` loads one, from 0; other formats have only page 0. The
[TIFF to PDF recipe](https://www.jadev-corp.com/recipes/tiff-to-pdf/) puts every page of a TIFF on its own PDF page.
## Size and placement
`DrawImage(image, x, y, width, height)` stretches the image to the rectangle. `DrawImage(image, x,
y)` draws it at its natural size: its pixels at the resolution the file records, else 96 per inch.
`Width` and `Height` give that natural size in points.
```csharp
Console.WriteLine($"photo.jpg: {photo.PixelWidth}x{photo.PixelHeight} pixels, {photo.Width:F0}x{photo.Height:F0} points at natural size");
```
## Fit, alignment and opacity
`DrawImage(image, x, y, width, height, fit, alignment, opacity)` fits an image into a box the way
CSS `object-fit` does, places it inside the box where it does not fill it, clips whatever spills
over, and fades it as a whole on top of its own alpha.
```csharp
// Each box is 120 by 120 points; the photo is 640 by 400 pixels.
PdfImageFit[] fits = [PdfImageFit.Fill, PdfImageFit.Contain, PdfImageFit.Cover, PdfImageFit.None];
foreach (PdfImageFit fit in fits)
{
g.DrawImage(webp, x, 60, 120, 120, fit, PdfImageAlignment.Center);
g.DrawRectangle(frame, null, x, 60, 120, 120);
g.DrawString(fit.ToString(), label, 10, PdfColor.Black, x, 196);
x += 130;
}
// Aligned to the top left of its box, and faded to 40 percent.
g.DrawImage(webp, 40, 220, 250, 120, PdfImageFit.Contain, PdfImageAlignment.TopLeft, opacity: 0.4);
```

| `PdfImageFit` | Effect |
|---|---|
| `Fill` | Stretched to the box, aspect ratio ignored |
| `Contain` | Scaled to fit inside the box, aspect ratio kept: the whole image shows |
| `Cover` | Scaled to cover the box, aspect ratio kept: what spills over is clipped |
| `None` | At its natural size, clipped to the box |
| `ScaleDown` | At its natural size, or as `Contain` when that is smaller |
`PdfImageAlignment` places the image: `TopLeft`, `Top`, `TopRight`, `Left`, `Center` (the default),
`Right`, `BottomLeft`, `Bottom` or `BottomRight`.
Images follow the current transform and clip of the canvas, so `RotateTransform`, `ScaleTransform`
and `IntersectClip` work on them as on shapes. See [Drawing](https://www.jadev-corp.com/guides/drawing/).
## SVG
`PdfSvg.Load` reads a file, a stream or bytes, and `PdfSvg.Parse` reads a string. `DrawSvg(svg, x,
y, width, height)` scales the drawing into the rectangle; its `viewBox` and
`preserveAspectRatio` decide how it fits.
Supported: shapes, paths with arcs, groups, `use` and `symbol`, nested viewports, transforms, solid
colors, linear and radial gradients, clip paths, opacity, dashes, text in the standard fonts,
embedded JPEG and PNG images, and `
```
- `src` takes TrueType, OpenType, WOFF and WOFF2 files, and `local()` names an installed font or
one from the `Fonts` collection. Sources are tried in order; a damaged file is skipped for the
next one.
- `font-weight` takes one weight or a range, and `font-style` italic or oblique. The face whose
style and weight match best is used, as CSS matches them.
- Only the glyphs the document draws are embedded, as a subset.
## How a font is chosen
For each family in `font-family`, in order:
1. `@font-face` rules of the document.
2. The `Fonts` option, a `PdfFontCollection` you fill from files or folders.
3. Installed fonts, unless `UseSystemFonts` is false.
The generic families map to the 14 standard PDF fonts, which need no embedding: `serif` (also
`cursive`, `fantasy` and `math`) to Times, `sans-serif` and `system-ui` to Helvetica, and
`monospace` to Courier. Common families that are not found, such as Arial, Roboto, Times New
Roman or Consolas, fall back to the standard font with the same role. A document with no font
named at all uses Times, as browsers default to serif.
Characters the chosen font lacks come from the next family in the list that has them, so
`Inter, "Noto Sans Arabic"` sets Latin text in Inter and Arabic in Noto Sans Arabic. With
`UseSystemFonts` on, installed fonts with wide coverage, such as Segoe UI, Noto Sans and DejaVu
Sans, back up any character still missing.
For output that is identical on every machine, such as on a build server or in a container, set
`UseSystemFonts = false` and ship the fonts with the application, in `@font-face` or in `Fonts`.
See [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/) for the font engine itself.
## Images
```html
```
| Format | Notes |
|---|---|
| JPEG | Embedded as is, without recompression |
| PNG | Every color type and bit depth, interlaced or not, transparency kept |
| GIF | The first frame, with its transparent color |
| SVG | Drawn as PDF vectors, sharp at any zoom, without filters, masks or patterns. Works in ``, in CSS backgrounds and inline as `