# 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"); ``` ![The page this code draws: a dark blue band with the words Hello, PDF](https://www.jadev-corp.com/previews/hello.webp) 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); ``` ![The PDF this code writes: the heading Invoice 42 and a total in bold](https://www.jadev-corp.com/previews/invoice-html.webp) 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"); ``` ![The page this code draws: a dark blue band with the words Hello, PDF](https://www.jadev-corp.com/previews/hello.webp) 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"); ``` ![A page with a blue header band and the words Hello, PDF](https://www.jadev-corp.com/previews/hello.webp) ## 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. ![Shapes, gradients, a rotated pattern and a clipped circle drawn with PdfNetStack](https://www.jadev-corp.com/previews/drawing.webp) ## 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"); ``` ![A report page with a light blue letterhead band drawn behind its text](https://www.jadev-corp.com/previews/drawing-background.webp) 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); ``` ![Text in Helvetica and Inter, an Arabic paragraph with an amount in euros, and justified Arabic with kashida](https://www.jadev-corp.com/previews/fonts.webp) ## 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); ``` ![The same words drawn from one variable font at the Thin, Regular, 550 and Bold weights](https://www.jadev-corp.com/previews/fonts-variable.webp) - 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 ``` ![A photo, a transparent PNG logo and an SVG badge placed on a page](https://www.jadev-corp.com/previews/images.webp) ## 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); ``` ![One photo fitted into four boxes with Fill, Contain, Cover and None, then faded to 40 percent](https://www.jadev-corp.com/previews/images-fit.webp) | `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 Landscape photograph Logo The photograph as WebP ``` | 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 `` | | BMP | 1 to 32 bits per pixel, with alpha | | TIFF | The first page; fax and JPEG strips, LZW, Deflate and PackBits, 1 to 16 bit samples | | WebP | Lossy and lossless, with alpha, the first frame of an animation | Images work in ``, `background-image`, `list-style-image` and `content: url()`. Each image is written once per document however often it appears. Intrinsic sizes follow the pixel density a raster image records, else 96 pixels per inch. `object-fit` and `object-position` place an image in its box. ## Data URIs `data:` URIs work everywhere a URL does, base64 or percent encoded, so a template can carry its images and fonts inside the HTML and need no files at all: ```html ``` ## Limits - SVG filters, masks and patterns are not drawn. - A resource larger than 64 MiB is not loaded. - With `UseSystemFonts` off, text needs a supplied font that covers its characters: the standard 14 fonts cover Western and Central European languages only. ## Related - [HTML to PDF overview](https://www.jadev-corp.com/guides/html-to-pdf/) - [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/) - [Images](https://www.jadev-corp.com/guides/images/) - API: [HtmlConverterOptions](https://www.jadev-corp.com/api/pdfnetstack-html/htmlconverteroptions/), [PdfFontCollection](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdffontcollection/) ## Does HTML to PDF download resources from the internet? Not by default. Without a ResourceLoader only data URIs and files under the base folder load. A loader you supply decides, address by address, what is fetched. ## Which font formats work with @font-face? TrueType, OpenType, WOFF and WOFF2. Only the glyphs a document uses are embedded. Source: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/fonts-and-assets/ --- # Page headers, footers and numbers in HTML > Control PDF pages from CSS in .NET with @page rules, margin boxes, page counters, named pages, running headers and breaks. Pure C#, MIT licensed. Available since 1.1. PdfNetStack.Html splits a document into pages with the CSS paged media rules: page size and margins, headers and footers in the margins, page numbers and breaks. ## Short answer Write an `@page` rule with a size, margins and margin boxes. `counter(page)` and `counter(pages)` give the page number and the page count, `string()` and `element()` carry text or whole elements from the document into the margins, and `break-before`, `break-after` and `break-inside` control where pages end. ```css @page { size: A4; margin: 22mm 18mm 24mm; @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 8pt } } ``` The `PageStyle` option sets a size and margins that apply before the document's own `@page` rules; its default is `size: A4; margin: 2cm`. ## A complete report This style sheet drives a four page report: a chapter title in the top left margin, a running element in the top right, alternating footers on left and right pages, a table that continues over two pages with its header repeated, a landscape annex and a watermark on every page. ```csharp const string Css = """ @page { size: A4; margin: 22mm 18mm 24mm; font-family: sans-serif; @top-left { content: string(chapter); font-size: 8pt; color: #64748b } @top-right { content: element(brand) } @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 8pt } } @page :first { @top-left { content: none } } @page :left { @bottom-left { content: "Quarterly report"; font-size: 8pt; color: #64748b } } @page :right { @bottom-right { content: "Quarterly report"; font-size: 8pt; color: #64748b } } @page wide { size: A4 landscape } body { font-family: sans-serif; font-size: 10pt } .brand { position: running(brand); text-align: right; font-size: 8pt; font-weight: bold; color: #1e3a8a } .draft { position: fixed; z-index: -1; top: 110mm; left: 25mm; font-size: 90pt; color: #e0e7ff; transform: rotate(-30deg) } h1 { string-set: chapter content(text); color: #1e3a8a } .chapter + .chapter { break-before: page } .annex { page: wide } h2 { break-after: avoid } p { orphans: 3; widows: 3 } table { width: 100%; border-collapse: collapse; font-size: 9pt } thead th { background: #1e3a8a; color: white; text-align: left; padding: 4pt 6pt } td { padding: 3pt 6pt; border-bottom: 0.5pt solid #cbd5e1 } tr { break-inside: avoid } """; ``` ![First page of the report: brand name in the top margin, a table with a blue header row, a DRAFT watermark behind the table and a page number footer](https://www.jadev-corp.com/previews/html-paged.webp) ## Page size and margins `size` takes a length pair or a named size, optionally with `landscape` or `portrait`. Named sizes are `A5`, `A4`, `A3`, `B5`, `B4`, `JIS-B5`, `JIS-B4`, `letter`, `legal` and `ledger`. `margin` works as on any box. Properties set on `@page` apply to its margin boxes, so a `font-family` there sets the font of every header and footer. ## Margin boxes The sixteen margin boxes of CSS Paged Media sit around the page area: `@top-left-corner`, `@top-left`, `@top-center`, `@top-right`, `@top-right-corner`, the same four along the bottom, and `@left-top`, `@left-middle`, `@left-bottom`, `@right-top`, `@right-middle` and `@right-bottom` on the sides. Each takes `content` and the usual text and box properties. The boxes along one side share it. A center box alone takes the whole side, and beside others a third. Without a center box, a left or right box alone takes the whole side and two take half each, so a single running header spans the page. ## First, left and right pages ```css @page :first { @top-left { content: none } } @page :left { @bottom-left { content: "Quarterly report"; font-size: 8pt; color: #64748b } } @page :right { @bottom-right { content: "Quarterly report"; font-size: 8pt; color: #64748b } } ``` `:first` matches the first page. In a left to right document the first page is a right page, so `:right` matches odd pages and `:left` even ones; a right to left document swaps them. A page rule with a pseudo-class outweighs one without, and a named page rule outweighs both. ## Page numbers `counter(page)` is the current page number and `counter(pages)` the total. Both take a counter style, such as `counter(page, lower-roman)`. They work in margin boxes, in running elements and in generated content in the body. ## Named pages A named page gives a section its own page type, and the section is laid out at that page's width: ```css @page wide { size: A4 landscape } .annex { page: wide } ``` A change of page name forces a page break, and the content after it is laid out at its own page type's width. The `page` property works on any block in the block flow, nested or not: in the report above, the annex section sits inside another `div`. A box that spans the break is sliced, its top edge before the break and its bottom edge after. Inside a flex, grid or table container `page` has no effect, and the container stays on one page type. ## Running headers from the document Named strings copy text from the document into the margins. `string-set` assigns the string where an element stands, and `string()` shows it: ```css h1 { string-set: chapter content(text); color: #1e3a8a } @page { @top-left { content: string(chapter) } } ``` `string-set` takes quoted text, `content(text)` and `attr()`. `string()` takes a second argument: `first` (the default, the first assignment on the page or else the last one before it), `start`, `last` or `first-except`. Running elements put any content in the margins: a logo, a table or styled text. ```css .brand { position: running(brand); text-align: right; font-size: 8pt; font-weight: bold; color: #1e3a8a } @page { @top-right { content: element(brand) } } ``` The element leaves the normal flow. Each page shows the running element that stands on it, else the last one before it, so chapter headers follow the chapters. `element()` takes `first`, `start`, `last` or `first-except` as `string()` does, and `counter(page)` and `counter(pages)` inside a running element's generated content show that page's numbers. ## Breaks ```css .chapter + .chapter { break-before: page } h2 { break-after: avoid } p { orphans: 3; widows: 3 } tr { break-inside: avoid } ``` - `break-before` and `break-after` with `page`, `left`, `right`, `recto` or `verso` force a page break. `avoid` keeps a heading with what follows. The legacy `page-break-before`, `page-break-after` and `page-break-inside` map to the same properties. - `break-inside: avoid` keeps a row, a figure or a block on one page when it fits. - `orphans` and `widows` set the minimum number of lines a paragraph leaves at the bottom of a page and carries to the top of the next. Both default to 2. - A forced break before the first content or after the last adds no empty page, so `break-before: page` on the first element still starts the document on page one. ## Repeated table headers and fixed boxes The rows of a `thead` repeat at the top of every page a table continues on; the report above shows its header on both pages of the table. A `position: fixed` box repeats on every page, which suits letterheads and watermarks. With a negative `z-index` it paints under the content of every page, so a watermark sits behind the table: ```css .draft { position: fixed; z-index: -1; top: 110mm; left: 25mm; font-size: 90pt; color: #e0e7ff; transform: rotate(-30deg) } ``` Repeated table headers and fixed boxes are marked as artifacts in the tagged output. ## Limits - The `page` property breaks between blocks of the block flow only: inside a flex, grid or table container it has no effect. - An inline running element shows on every page rather than from where it stands. - `target-counter()` and `leader()`, used for tables of contents with page numbers, are not supported. - No multi-column layout. ## Related - [HTML to PDF overview](https://www.jadev-corp.com/guides/html-to-pdf/) - [Fonts and assets](https://www.jadev-corp.com/guides/html-to-pdf/fonts-and-assets/) - [CSS support](https://www.jadev-corp.com/guides/html-to-pdf/css-support/) - API: [HtmlConverterOptions](https://www.jadev-corp.com/api/pdfnetstack-html/htmlconverteroptions/) ## How do I add page numbers when converting HTML to PDF in C#? Put counter(page) and counter(pages) in a margin box of an @page rule, for example @bottom-center { content: "Page " counter(page) " of " counter(pages) }. ## Do table headers repeat on every page? Yes. The rows of a thead repeat at the top of every page a table continues on. Source: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/paged-media/ --- # Low level PDF objects in C# > Read and change any part of a PDF from C# with PdfDictionary, PdfArray, PdfStream and friends, for what the high level API does not cover. MIT. Available since 1.1. Reach every dictionary, array and stream of a PDF when the high level API does not cover what you need. ## Short answer `doc.Catalog` is the root of the object graph and `page.Dictionary` is a page's own dictionary. `doc.Resolve` follows references, `doc.AddObject` adds new indirect objects, and `doc.GetStreamData` decodes a stream. Changes are written by the next `Save`. ```csharp PdfDocument doc = PdfDocument.Open("report.pdf"); doc.Catalog.Set("PageMode", new PdfName("UseOutlines")); // open with the bookmarks panel shown PdfPage page = doc.Pages[0]; if (doc.Resolve(page.Dictionary["Resources"]) is PdfDictionary resources && doc.Resolve(resources["Font"]) is PdfDictionary fonts) { Console.WriteLine($"Fonts on page 1: {string.Join(", ", fonts.Keys)}"); } ``` ## The object types The types live in the `PdfNetStack.Objects` namespace and mirror ISO 32000-1, section 7.3. | Type | Holds | Create with | |---|---|---| | `PdfNull` | The null object | `PdfNull.Instance` | | `PdfBoolean` | true or false | `PdfBoolean.True`, `PdfBoolean.Get(value)` | | `PdfNumber` | An integer or a real, kept as canonical text so values read from a file survive a round trip exactly | `PdfNumber.FromInteger(42)`, `PdfNumber.FromDouble(0.5)` | | `PdfName` | A name such as `/Type`, without the slash in `Value` | `new PdfName("Type")` | | `PdfString` | Bytes, which may be text | `PdfString.FromText("Hello")`; read with `ToText()` | | `PdfArray` | An ordered list | `new PdfArray()`, `PdfArray.Of(...)` | | `PdfDictionary` | Keys without the slash, in insertion order | `new PdfDictionary { ["Type"] = new PdfName("Annot") }` | | `PdfStream` | A dictionary plus encoded data | `PdfStream.FromData(bytes)`, Flate compressed unless `compress: false` | | `PdfReference` | A reference to an indirect object of one document | Returned by `doc.AddObject` or found in the document | A dictionary's indexer returns null for a missing key, and setting null removes the entry. `GetName(key)` returns a name value without its slash. ## Read Values in dictionaries and arrays may be references. Pass them through `doc.Resolve`, which returns the object itself when it is not a reference, and `PdfNull.Instance` for a reference to a missing object. ```csharp // /Contents is one stream or an array of streams. PdfObject? contents = doc.Resolve(page.Dictionary["Contents"]); IEnumerable parts = contents is PdfArray array ? array : [contents!]; foreach (PdfObject part in parts) { if (doc.Resolve(part) is PdfStream stream) { byte[] content = doc.GetStreamData(stream); // filters undone, image codecs kept Console.WriteLine($"Content stream: {content.Length:N0} bytes decoded, {stream.Data.Length:N0} stored"); } } ``` `GetStreamData` undoes the Flate, LZW, ASCIIHex, ASCII85 and RunLength filters and the PNG and TIFF predictors. Image codecs (DCTDecode, JPXDecode, JBIG2Decode, CCITTFaxDecode) are left in place, so an image stream comes back as its JPEG or JPEG 2000 file; for pixels, use [`PdfPageImage.Decode`](https://www.jadev-corp.com/guides/extract-images/). Damaged data, or data over the document's decode limit, raises `PdfFormatException`. ## Write Streams are always indirect objects: add them with `doc.AddObject` and store the returned reference. An added object is written on save only if something references it. ```csharp // A second content stream that draws a blue square over the page. PdfStream square = PdfStream.FromData("q 0 0 1 rg 72 72 144 144 re f Q"u8); PdfObject existing = page.Dictionary["Contents"]!; page.Dictionary.Set("Contents", PdfArray.Of(existing, doc.AddObject(square))); doc.Info["Reviewed"] = "yes"; doc.Save("report-square.pdf"); ``` The square in this sample is placed in PDF default user space, from the bottom left of the page. The [drawing canvas](https://www.jadev-corp.com/guides/drawing/) does the same job with top left coordinates and keeps track of resources for you; see [Coordinates and units](https://www.jadev-corp.com/concepts/coordinates-and-units/). ## What is already on the object `page.Dictionary` already carries the inherited attributes (resources, boxes, rotation), copied onto each page when the document opens. `doc.Catalog` is the document catalog, where viewer settings, page labels, named destinations, outlines and the form live. Most of these have a high level API too: `doc.Viewer`, `doc.PageLabels`, `doc.NamedDestinations`, `doc.Outlines` and `doc.Form`. ## Limits - References belong to one document. Use `Pages.Import` to move pages between documents rather than copying references. - Saving rebuilds the page tree as one flat node and writes only objects reachable from the catalog, the information dictionary and the document ID. - The object model does not check what you write against the specification. A dictionary with a wrong key is saved as is. ## Related - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/): the high level editing API - [Drawing](https://www.jadev-corp.com/guides/drawing/): content streams without writing operators by hand - [Architecture](https://www.jadev-corp.com/concepts/architecture/): how the parser and writer use these types - API: [PdfDictionary](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfdictionary/), [PdfArray](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfarray/), [PdfStream](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfstream/), [PdfReference](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfreference/), [PdfDocument](https://www.jadev-corp.com/api/pdfnetstack/pdfdocument/) Source: https://www.jadev-corp.com/pdfnetstack/guides/low-level-objects/ --- # Recipes > Short answers to common PDF tasks in C# and .NET with PdfNetStack, each with a complete program that CI compiles and runs. One task per page: a short answer and a complete program you can paste and run. Every sample is compiled and run against the library on each build of this site. The guides cover each area in depth. ## Pages and documents | Recipe | In one line | |---|---| | [Merge PDF files](https://www.jadev-corp.com/recipes/merge-pdf-files/) | Combine several PDFs into one with `PdfDocument.Merge` | | [Split a PDF into one file per page](https://www.jadev-corp.com/recipes/split-pdf/) | One document per page with `Split` | | [Extract pages from a PDF](https://www.jadev-corp.com/recipes/extract-pdf-pages/) | Copy chosen pages into a new file, in any order | | [Rotate PDF pages](https://www.jadev-corp.com/recipes/rotate-pdf-pages/) | Turn pages by a multiple of 90 degrees | | [Add page numbers](https://www.jadev-corp.com/recipes/add-page-numbers/) | Draw "Page 1 of 3" at the foot of every page | | [Add a watermark](https://www.jadev-corp.com/recipes/add-watermark/) | A rotated, semi transparent word across every page | ## Size and quality | Recipe | In one line | |---|---| | [Reduce PDF size for email](https://www.jadev-corp.com/recipes/reduce-pdf-size/) | Downsample oversized images and store photographs as JPEG | | [Downsample images in a PDF](https://www.jadev-corp.com/recipes/downsample-pdf-images/) | Bring every image down to the resolution it is shown at | | [Linearize a PDF for fast web view](https://www.jadev-corp.com/recipes/linearize-pdf/) | Show the first page before the whole file has downloaded | | [Batch compress PDFs from the command line](https://www.jadev-corp.com/recipes/batch-compress-pdf-cli/) | A whole folder with `pdfnetstack batch` and a CSV report | | [Check two PDFs for equivalence](https://www.jadev-corp.com/recipes/check-pdf-equivalence/) | Prove another tool's output holds the same document | ## Security, forms and archiving | Recipe | In one line | |---|---| | [Password protect a PDF](https://www.jadev-corp.com/recipes/password-protect-pdf/) | AES-256 with user and owner passwords and permissions | | [Remove a password from a PDF](https://www.jadev-corp.com/recipes/remove-pdf-password/) | Open with the owner password and save unprotected | | [Fill and flatten a PDF form](https://www.jadev-corp.com/recipes/fill-and-flatten-pdf-form/) | Set field values, then stamp them into the page | | [Flatten annotations](https://www.jadev-corp.com/recipes/flatten-pdf-annotations/) | Make comments, highlights and stamps part of the page | | [Add a signature field](https://www.jadev-corp.com/recipes/add-signature-field/) | An empty box for a signer to sign in their viewer | | [Create a Factur-X invoice](https://www.jadev-corp.com/recipes/pdf-a-3-xml-attachment/) | A PDF/A-3 e-invoice with its XML and Factur-X metadata | ## Content | Recipe | In one line | |---|---| | [Convert an HTML invoice to PDF](https://www.jadev-corp.com/recipes/html-invoice-to-pdf/) | HTML and CSS to PDF with page numbers, no browser | | [Convert a TIFF to PDF](https://www.jadev-corp.com/recipes/tiff-to-pdf/) | Every page of a multi-page TIFF on its own PDF page | | [Write Arabic text in a PDF](https://www.jadev-corp.com/recipes/arabic-text-pdf/) | Right to left text with joined letters, searchable | | [Extract all images from a PDF](https://www.jadev-corp.com/recipes/extract-images-from-pdf/) | JPEGs as stored, other images as PNG | | [Make a scanned PDF searchable](https://www.jadev-corp.com/recipes/make-scanned-pdf-searchable/) | OCR with the `PdfNetStack.Ocr` add-on | Recipes marked 1.1 need the 1.1 packages or later. Source: https://www.jadev-corp.com/pdfnetstack/recipes/ --- # Add page numbers to a PDF in C# > Stamp "Page 1 of 3" at the foot of every page of an existing PDF in .NET with the PdfNetStack drawing canvas. MIT licensed, pure C#. Available since 1.1. Number the pages of an existing PDF, centered at the bottom of each page. ## Short answer Open the document, call `CreateGraphics` on each page, and draw the label with `DrawString`. `MeasureString` gives the label's width so it can be centered. The canvas origin is the top left corner of the page, y grows downward, and units are points. ```csharp title="recipe-add-page-numbers.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; using PdfNetStack.Fonts; PdfDocument doc = PdfDocument.Open("report.pdf"); PdfFont font = PdfStandardFont.Helvetica; for (int i = 0; i < doc.Pages.Count; i++) { PdfPage page = doc.Pages[i]; string label = $"Page {i + 1} of {doc.Pages.Count}"; double width = font.MeasureString(label, 9); using PdfGraphics g = page.CreateGraphics(); // origin top left, y down, points g.DrawString(label, font, 9, PdfColor.Gray(0.35), (page.Width - width) / 2, page.Height - 30); } doc.Save("report-numbered.pdf"); ``` ## Variations - Use an embedded font for the label: `PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2")`. - To start the visible numbering elsewhere or use roman numerals in viewers, set `doc.PageLabels` as well. Page labels change what viewers show in their page box, not the page content. - New documents produced from HTML can number pages with `counter(page)` and `counter(pages)` in `@page` margin boxes, see [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/). ## Related - [Drawing](https://www.jadev-corp.com/guides/drawing/) - [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/) - API: [PdfGraphics](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfgraphics/), [PdfFont](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdffont/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/add-page-numbers/ --- # Add a signature field to a PDF in C# > Add an empty signature field to a PDF in .NET so a signer can sign it in their PDF viewer, with border and tooltip. MIT licensed, pure C#. Available since 1.1. Place an empty signature box on a page for someone to sign later in their PDF viewer. ## Short answer Call `doc.Form.AddSignatureField` with the page, a field name and the box position and size, measured from the page's top left corner in points. `PdfFieldOptions` sets the border, background and tooltip. ```csharp title="recipe-add-signature-field.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; PdfDocument doc = PdfDocument.Open("contract.pdf"); PdfPage last = doc.Pages[^1]; doc.Form.AddSignatureField(last, "signature", 72, 730, 220, 60, new PdfFieldOptions { BorderColor = PdfColor.Gray(0.5), ToolTip = "Sign here", }); doc.Save("contract-to-sign.pdf"); Console.WriteLine(string.Join(", ", PdfDocument.Open("contract-to-sign.pdf").Form.Fields.Select(f => $"{f.Name} ({f.Kind})"))); ``` ## Notes To sign in code instead, use `PdfSigner.Sign`. It adds its own signature field, with an optional visible appearance, in an incremental update, and does not fill an existing empty field. See [Signatures](https://www.jadev-corp.com/guides/signatures/). ## Related - [Forms](https://www.jadev-corp.com/guides/forms/) - [Signatures](https://www.jadev-corp.com/guides/signatures/) - API: [PdfAcroForm](https://www.jadev-corp.com/api/pdfnetstack/pdfacroform/), [PdfFieldOptions](https://www.jadev-corp.com/api/pdfnetstack/pdffieldoptions/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/add-signature-field/ --- # Add a watermark to a PDF in C# > Stamp a diagonal, semi transparent DRAFT watermark on every page of a PDF in .NET with the PdfNetStack canvas. MIT licensed, pure C#. Available since 1.1. Write a large, rotated, semi transparent word across every page. ## Short answer Draw on each page with `CreateGraphics`, rotate the canvas about the page center with `RotateAtTransform`, and draw the text with a color that has an alpha below 1. Positive angles turn clockwise, so -45 runs the text up from bottom left to top right. ```csharp title="recipe-add-watermark.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; using PdfNetStack.Fonts; PdfDocument doc = PdfDocument.Open("contract.pdf"); PdfFont font = PdfStandardFont.HelveticaBold; double size = 110; double width = font.MeasureString("DRAFT", size); foreach (PdfPage page in doc.Pages) { using PdfGraphics g = page.CreateGraphics(); g.RotateAtTransform(-45, page.Width / 2, page.Height / 2); // positive angles turn clockwise g.DrawString("DRAFT", font, size, PdfColor.FromHex("#DC2626").WithAlpha(0.25), (page.Width - width) / 2, page.Height / 2 + size * 0.35); } doc.Save("contract-draft.pdf"); ``` ![The first page of the contract with a red DRAFT watermark across it](https://www.jadev-corp.com/previews/recipe-watermark.webp) ## Variations - `page.CreateGraphics(PdfGraphicsLayer.Background)` draws underneath the existing content, so the text of the page stays on top of the watermark. - For an image or a logo, use `DrawImage` or `DrawSvg` the same way. To stamp another PDF page, such as a letterhead, use `PdfForm.FromPage` and `DrawForm`, see [Images](https://www.jadev-corp.com/guides/images/). ## Related - [Drawing](https://www.jadev-corp.com/guides/drawing/) - [Images](https://www.jadev-corp.com/guides/images/) - API: [PdfGraphics](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfgraphics/), [PdfColor](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfcolor/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/add-watermark/ --- # Write Arabic text in a PDF in C# > Draw Arabic and other right to left text in a PDF in .NET with joined letters, bidi ordering and searchable text. MIT licensed, pure C#. Available since 1.1. Write a right to left paragraph with an embedded Arabic font, numbers and currency included. ## Short answer Load a font that covers Arabic with `PdfOpenTypeFont.Load`, put the text in a `PdfParagraph` with `Direction = PdfTextDirection.RightToLeft`, and draw it with `DrawText`. Letters join and take their contextual forms, and the text carries ActualText, so it copies and searches in logical order. ```csharp title="recipe-arabic-text-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; using PdfNetStack.Fonts; PdfFont arabic = PdfOpenTypeFont.Load("fonts/NotoSansArabic-Regular.woff2"); // embedded as a subset PdfFont latin = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2"); var style = new PdfTextStyle(arabic, 20) { Fallbacks = [latin] }; // digits and the euro sign PdfDocument doc = PdfDocument.Create(); PdfPage page = doc.Pages.Add(PdfRectangle.A4); using (PdfGraphics g = page.CreateGraphics()) { var paragraph = new PdfParagraph { Direction = PdfTextDirection.RightToLeft, LineHeight = 1.6 } .Add("المبلغ المستحق: 1,250.00 € خلال ثلاثين يوما من تاريخ الفاتورة.", style); g.DrawText(paragraph, 60, 60, page.Width - 120); // top left corner and width } doc.Save("arabic.pdf"); Console.WriteLine(PdfDocument.Open("arabic.pdf").Pages[0].ExtractText()); // logical order ``` ![An Arabic sentence drawn right to left at the top of a page](https://www.jadev-corp.com/previews/recipe-arabic.webp) ## Notes - The font is embedded as a subset of the glyphs drawn. TrueType, OpenType, WOFF and WOFF2 files load the same way. - A font that lacks some characters, such as Latin letters, digits or the euro sign, borrows them from the fonts in `Fallbacks` on the `PdfTextStyle`, as the sample does with Inter. - Numbers keep their left to right order inside the right to left line, as the Unicode bidirectional algorithm places them. - `doc.Viewer.RightToLeft = true` tells viewers the document reads right to left, which sets the order of pages shown side by side. ## Related - [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/) - [Extract text](https://www.jadev-corp.com/guides/extract-text/) - API: [PdfParagraph](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfparagraph/), [PdfTextStyle](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdftextstyle/), [PdfOpenTypeFont](https://www.jadev-corp.com/api/pdfnetstack-fonts/pdfopentypefont/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/arabic-text-pdf/ --- # Batch compress PDFs from the command line > Compress every PDF in a folder on Windows, Linux or macOS with the pdfnetstack tool, losslessly, with a CSV report per file. MIT licensed. Available since 1.0. Optimize a whole folder of PDFs in one command, with a report of what each file gained. ## Short answer Install the `pdfnetstack` tool and run `batch` with an input and an output folder. Each file is optimized losslessly and verified, and `--report` writes one CSV row per document. Without an output folder, `batch` only measures and writes nothing. ```bash dotnet tool install -g PdfNetStack.Tool pdfnetstack batch ./incoming ./optimized --report report.csv pdfnetstack batch ./incoming # measure only, writes nothing pdfnetstack batch ./incoming ./optimized --jpeg-quality 60 --jobs 4 ``` `--jobs` sets how many documents run in parallel, `--threads` the threads per document. The tool needs the .NET 8 or later runtime. ## The same in C# ```csharp title="recipe-batch-compress-pdf-cli.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; Directory.CreateDirectory("optimized"); foreach (string path in Directory.GetFiles(".", "*.pdf")) { string output = Path.Combine("optimized", Path.GetFileName(path)); PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync(path, output); Console.WriteLine($"{Path.GetFileName(path)}: {result.Disposition}, {result.SavingPercent:F1}% saved"); } ``` Files that are already well compressed come back as `OriginalRetained`: the rewrite saved less than `MinSavingPercent` (1% by default), so the original bytes were kept. ## Related - [Command line](https://www.jadev-corp.com/cli/): every command and option - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/) - API: [PdfOptimizer](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizer/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/batch-compress-pdf-cli/ --- # Check two PDFs for equivalence in C# > Check another PDF tool's output against its input in .NET: same objects, same decoded content, JPEGs compared by coefficient. MIT licensed. Available since 1.0. Find out whether a PDF written by any tool still holds the same document as the original. ## Short answer `PdfEquivalence.FindDifference` walks both object graphs from the trailer and compares every dictionary, array and reference, and every stream after decoding. It returns `null` when the two are equivalent, or a description of the first difference. ```csharp byte[] original = File.ReadAllBytes("report.pdf"); foreach (string candidate in new[] { "report-optimized.pdf", "report-edited.pdf" }) { string? difference = PdfEquivalence.FindDifference(original, File.ReadAllBytes(candidate)); Console.WriteLine($"{candidate}: {difference ?? "equivalent"}"); } ``` ```bash dotnet add package PdfNetStack ``` In the sample, the optimized copy is equivalent and the copy with a page removed is not: `trailer/Root/Pages/Kids: array length 3 vs 2`. ## Notes This is the check the optimizer runs on its own output. Encoding keys such as `/Length`, `/Filter` and `/DecodeParms` may differ. JPEG images must match coefficient by coefficient; pass `allowJpegRequantization: true` to accept requantized JPEGs. From 1.1, an overload that also takes `minImageSsim` accepts images that another tool downsampled or converted to JPEG, when they match the source resampled to their size; soft masks, masks and stencil images must still match exactly. The method and its limits are on the [fidelity](https://www.jadev-corp.com/concepts/fidelity/) page. ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/) - [Command line](https://www.jadev-corp.com/cli/): `pdfnetstack verify original.pdf other.pdf` - API: [PdfEquivalence](https://www.jadev-corp.com/api/pdfnetstack/pdfequivalence/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/check-pdf-equivalence/ --- # Downsample images in a PDF in C# > Lower the resolution of oversized images in a PDF from .NET with MaxImageDpi, measured where each image is drawn, the result verified. MIT licensed. Available since 1.1. Bring every image down to the resolution the page actually shows it at. ## Short answer Set `MaxImageDpi` on `PdfOptimizerOptions`. The optimizer measures each image at the largest size it is drawn anywhere in the document, and resamples images drawn above 1.5 times the target down to the target, with a Lanczos filter. A raw image stays lossless after resampling and a JPEG stays a JPEG, so nothing changes format unless you also set `ConvertImagesToJpeg`. ```csharp var options = new PdfOptimizerOptions { MaxImageDpi = 150 }; PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("oversampled.pdf", "downsampled.pdf", options); Console.WriteLine($"{result.Statistics.ImagesDownsampled} image(s) downsampled, {result.SavingPercent:F1}% saved"); Console.WriteLine(result.Reason); ``` The sample's photograph, 1600 pixels drawn 3 inches wide (about 530 dpi), becomes a 150 dpi image and the file shrinks by 93.8%. ## Notes - Masks, bilevel, indexed, 16 bit, Lab, Separation and DeviceN images, JPEG 2000, JBIG2 and CCITT images, and images inside Type 3 glyphs keep their resolution. - Each downsampled image is decoded and compared with an exact resample of the original: same components and aspect, mean colors kept, and a minimum SSIM. `MinImageSsim` raises that minimum. - Command line: `pdfnetstack optimize in.pdf out.pdf --max-dpi 150`. ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/#lossy-image-options) - [Reduce PDF size for email](https://www.jadev-corp.com/recipes/reduce-pdf-size/) - [Command line](https://www.jadev-corp.com/cli/) - API: [PdfOptimizerOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizeroptions/), [OptimizationStatistics](https://www.jadev-corp.com/api/pdfnetstack/optimizationstatistics/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/downsample-pdf-images/ --- # Extract all images from a PDF in C# > Save every image of a PDF in .NET, JPEGs as stored and other images as PNG, with size and resolution. MIT licensed, pure C#, no native code. Available since 1.1. Save the images every page draws, as JPEG or PNG files. ## Short answer `GetImages` lists the images a page draws, with their placement and resolution. A JPEG (`DCTDecode`) is written as stored with `GetEncodedData`, so nothing is recompressed. Other images are decoded to 8 bit gray or RGB pixels with `Decode` and saved as PNG. ```csharp title="recipe-extract-images-from-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument doc = PdfDocument.Open("manual.pdf"); for (int p = 0; p < doc.Pages.Count; p++) { IReadOnlyList images = doc.Pages[p].GetImages(); for (int i = 0; i < images.Count; i++) { PdfPageImage image = images[i]; string name = $"page{p + 1}-image{i + 1}"; if (image.Encoding == "DCTDecode") { File.WriteAllBytes(name + ".jpg", image.GetEncodedData()); // the JPEG as stored } else if (image.Decode() is PdfBitmap bitmap) { bitmap.SavePng(name + ".png"); // 8 bit gray or RGB pixels } Console.WriteLine($"{name}: {image.Width}x{image.Height}, {image.Encoding ?? "raw"}, {image.HorizontalResolution:0} dpi"); } } ``` ## Notes - An image drawn on several pages is listed on each of them. `HorizontalResolution` and `VerticalResolution` give the dots per inch at which it is drawn. - `Decode` handles raw samples, JPEG, JPEG 2000, CCITT fax and JBIG2. Soft masks are not applied, and `Decode` returns null for an unsupported color space. ## Related - [Extract images](https://www.jadev-corp.com/guides/extract-images/) - [Extract text](https://www.jadev-corp.com/guides/extract-text/) - API: [PdfPageImage](https://www.jadev-corp.com/api/pdfnetstack/pdfpageimage/), [PdfBitmap](https://www.jadev-corp.com/api/pdfnetstack/pdfbitmap/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/extract-images-from-pdf/ --- # Extract pages from a PDF in C# > Copy a page range or a list of pages from a PDF into a new file in .NET, in any order. Shared resources are copied once. MIT licensed. Available since 1.0. Copy some pages of a PDF into a new document, in the order you choose. ## Short answer Create an empty document with `PdfDocument.Create`, then import the pages you want with `Pages.Import`. Page numbers in the sample start at 1. Fonts and images shared between the pages are copied once. ```csharp title="recipe-extract-pdf-pages.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument source = PdfDocument.Open("manual.pdf"); int[] keep = [1, 2, 5]; // page numbers, from 1, in the order you want them PdfDocument extract = PdfDocument.Create(); extract.Pages.Import(keep.Select(n => source.Pages[n - 1])); extract.Save("manual-extract.pdf"); Console.WriteLine($"manual-extract.pdf: {extract.Pages.Count} pages"); ``` ```bash dotnet add package PdfNetStack ``` ## Notes Annotations are copied fresh for every import. Links to pages imported in the same call are remapped, and links to pages left behind become empty. ## Related - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/) - [Command line](https://www.jadev-corp.com/cli/): `pdfnetstack extract in.pdf out.pdf 1-3,7,9-` takes ranges in any order, repeats allowed - API: [PdfPageCollection](https://www.jadev-corp.com/api/pdfnetstack/pdfpagecollection/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/extract-pdf-pages/ --- # Fill and flatten a PDF form in C# > Fill AcroForm text fields, check boxes, radio buttons and combo boxes in .NET, then flatten the values into the page. MIT licensed, pure C#. Available since 1.1. Fill an interactive PDF form from code, then make the values part of the page. ## Short answer Look fields up by name in `doc.Form`, cast to the field type, and set the value. Setting a value redraws the field's appearance, so the filled form looks the same in every viewer. `Flatten` stamps the appearances into the pages and removes the form, so the values can no longer be edited. ```csharp title="recipe-fill-and-flatten-pdf-form.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument doc = PdfDocument.Open("application.pdf"); ((PdfTextField)doc.Form["name"]!).Text = "Jamal Abdelkhalek"; ((PdfCheckBoxField)doc.Form["subscribe"]!).Checked = true; ((PdfRadioButtonField)doc.Form["plan"]!).Selected = "pro"; ((PdfChoiceField)doc.Form["country"]!).Select("Morocco"); doc.Form.Flatten(); // values become page content, the form is removed doc.Save("application-filled.pdf"); ``` ![The application form with a name, a checked box, the pro plan and Morocco filled in](https://www.jadev-corp.com/previews/recipe-filled-form.webp) ## Notes - Field names are fully qualified: a field `city` inside `address` is `address.city`. `doc.Form.Fields` lists them all. - Skip `Flatten` to keep the form editable after filling it. - A font the form embeds whole is reused for the new appearance; others map to the closest standard font. ## Related - [Forms](https://www.jadev-corp.com/guides/forms/): reading, filling, flattening and creating fields - API: [PdfAcroForm](https://www.jadev-corp.com/api/pdfnetstack/pdfacroform/), [PdfTextField](https://www.jadev-corp.com/api/pdfnetstack/pdftextfield/), [PdfChoiceField](https://www.jadev-corp.com/api/pdfnetstack/pdfchoicefield/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/fill-and-flatten-pdf-form/ --- # Flatten annotations in a PDF in C# > Turn comments, highlights, stamps and other PDF annotations into page content in .NET, so they print and show the same everywhere. MIT, pure C#. Available since 1.1. Make review marks part of the page, so nobody can move, edit or hide them. ## Short answer `PdfDocument.FlattenAnnotations` draws the appearance of every markup annotation (notes, text markup, free text, stamps, ink, shapes and file attachments) into its page's content and removes the annotation with its pop up. Links and form fields stay unless you ask for them. ```csharp PdfDocument doc = PdfDocument.Open("contract-reviewed.pdf"); int flattened = doc.FlattenAnnotations(); // every page; links and form fields stay doc.Save("contract-flat.pdf"); Console.WriteLine($"{flattened} annotations are now page content"); ``` To flatten a single annotation, pass it to its page's collection: ```csharp PdfDocument other = PdfDocument.Open("contract-reviewed.pdf"); PdfPage page = other.Pages[0]; PdfAnnotation stamp = page.Annotations.First(a => a.Subtype == "Stamp"); page.Annotations.Flatten(stamp); // only the stamp; the rest stay editable other.Save("contract-stamped.pdf"); ``` ## Notes - `new PdfFlattenOptions { IncludeLinks = true }` also flattens links: they keep their look and stop working. `IncludeWidgets = true` also flattens form fields, which show their current value and leave the form. `doc.Form.Flatten()` flattens a whole form. - Hidden annotations, and annotations without an appearance, stay as they are. ## Related - [Annotations](https://www.jadev-corp.com/guides/annotations/): adding, reading and flattening annotations - [Fill and flatten a PDF form](https://www.jadev-corp.com/recipes/fill-and-flatten-pdf-form/) - API: [PdfDocument](https://www.jadev-corp.com/api/pdfnetstack/pdfdocument/), [PdfAnnotationCollection](https://www.jadev-corp.com/api/pdfnetstack/pdfannotationcollection/), [PdfFlattenOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfflattenoptions/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/flatten-pdf-annotations/ --- # Convert an HTML invoice to PDF in C# > Turn an HTML and CSS invoice into a PDF in .NET with page numbers in the margin, without a browser or Chromium. MIT licensed, pure C#. Available since 1.1. Render an invoice written in HTML and CSS to a PDF, with a page footer. ## Short answer Pass the HTML to `HtmlConverter.ConvertToPdf` from the `PdfNetStack.Html` package. The `@page` rule sets the paper size and margins, and its `@bottom-center` margin box numbers the pages. No browser runs, and nothing is fetched from the network. ```csharp title="recipe-html-invoice-to-pdf.cs" #:package PdfNetStack.Html@1.2.0 using PdfNetStack.Html; string html = """ Invoice 42

Invoice 42

Issued 2026-10-29. Payment due within 30 days.

ItemAmount
Consulting, 8 hours800.00 EUR
Support plan200.00 EUR
Total1,000.00 EUR
"""; byte[] pdf = HtmlConverter.ConvertToPdf(html); File.WriteAllBytes("invoice-42.pdf", pdf); ``` ## Variations - `HtmlConverter.ConvertFileToDocument("invoice.html")` reads a file, and its style sheets, images and fonts from the same folder. - `ConvertToDocument` returns a `PdfDocument`, so the invoice can be signed, encrypted or saved as PDF/A before it is written. - Fonts declared with `@font-face` are embedded. The generic families `serif`, `sans-serif` and `monospace` map to the standard PDF fonts, which need no embedding. ## Related - [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) - [Create a Factur-X invoice](https://www.jadev-corp.com/recipes/pdf-a-3-xml-attachment/) - API: [HtmlConverter](https://www.jadev-corp.com/api/pdfnetstack-html/htmlconverter/), [HtmlConverterOptions](https://www.jadev-corp.com/api/pdfnetstack-html/htmlconverteroptions/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/html-invoice-to-pdf/ --- # Linearize a PDF for fast web view in C# > Write linearized PDFs in .NET so viewers show the first page before the whole file has downloaded, with every output verified. MIT licensed, pure C#. Available since 1.1. Lay a PDF out first page first, so a browser shows it while the rest is still downloading. ## Short answer Set `Linearize = true` in `PdfOptimizerOptions` and optimize the file. The output is linearized as ISO 32000-1 Annex F describes, with hint tables, and verified against the source like any other rewrite. `Statistics.Linearized` tells you whether it was. ```csharp title="recipe-linearize-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; var options = new PdfOptimizerOptions { Linearize = true }; PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("report.pdf", "report.web.pdf", options); Console.WriteLine($"{result.Disposition}, linearized: {result.Statistics.Linearized}"); Console.WriteLine(result.Reason); ``` On the command line: `pdfnetstack optimize report.pdf report.web.pdf --linearize`. ## Notes - A linearized file has no object streams, so it can be larger than the input. A linearized rewrite of a file that is not linearized yet is kept even then. - The other passes still run: unused resources go, content streams are compacted and duplicates merge. - A page tree that cannot be laid out page by page is written as usual, and the result's reason says why. ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/#fast-web-view) - [Command line](https://www.jadev-corp.com/cli/) - API: [PdfOptimizerOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizeroptions/), [OptimizationStatistics](https://www.jadev-corp.com/api/pdfnetstack/optimizationstatistics/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/linearize-pdf/ --- # Make a scanned PDF searchable in C# > Add an invisible OCR text layer to scanned PDFs in .NET with the PdfNetStack.Ocr add-on, Latin and Arabic, models checked by hash. MIT. Available since 1.1. Read the text of scanned pages and lay it over the scans, so the PDF selects, copies and searches. ## Short answer Load the OCR models with `OnnxOcrEngine.Load`, then call `PdfOcr.MakeSearchable` on the document and save it. Each page's scan is found, turned upright, read, and every word is laid over it as invisible text. The pages look the same as before. ```csharp title="recipe-make-scanned-pdf-searchable.cs" #:package PdfNetStack.Ocr@1.2.0 using PdfNetStack; using PdfNetStack.Ocr; // Models: python tools/fetch_ocr_models.py models (checked against pinned SHA-256 hashes). using var engine = OnnxOcrEngine.Load("models", new OnnxOcrEngineOptions { Languages = ["latin", "arabic"] }); PdfDocument doc = PdfDocument.Open("scan.pdf"); var ocr = new PdfOcr(engine, new PdfOcrOptions { Languages = ["latin", "arabic"] }); PdfOcrReport report = ocr.MakeSearchable(doc); // invisible text laid over each scan doc.Save("scan-searchable.pdf"); Console.WriteLine(report.Text); ``` ## Notes - `PdfNetStack.Ocr` is a separate package on NuGet. It runs the PP-OCRv5 mobile models on ONNX Runtime. - Get the models from the `PdfNetStack.Ocr.Models` package, or with `python tools/fetch_ocr_models.py models`. They are checked against pinned SHA-256 hashes when loaded. - A page qualifies when its scan is one image covering at least half the page. Pages that already have text are left alone. `Recognize` reports the text without changing the document. - ONNX Runtime's official builds send telemetry by default. The engine turns it off before loading a model; on Linux and macOS also set `ORT_DISABLE_TELEMETRY=1` before the process starts. ## Related - [OCR](https://www.jadev-corp.com/guides/ocr/) - [Extract text](https://www.jadev-corp.com/guides/extract-text/) - API: [PdfOcr](https://www.jadev-corp.com/api/pdfnetstack-ocr/pdfocr/), [OnnxOcrEngine](https://www.jadev-corp.com/api/pdfnetstack-ocr/onnxocrengine/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/make-scanned-pdf-searchable/ --- # Merge PDF files in C# > Combine several PDF files into one in .NET with PdfDocument.Merge. Shared fonts and images are written once. MIT licensed, pure C#. Available since 1.0. Combine several PDFs into one document, pages in the order you give. ## Short answer Open each file with `PdfDocument.Open`, pass them to `PdfDocument.Merge`, and save the result. Saving runs the lossless optimizer, so fonts and images the files share are written once. ```csharp title="recipe-merge-pdf-files.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; string[] files = ["a.pdf", "b.pdf", "c.pdf"]; PdfDocument merged = PdfDocument.Merge(files.Select(file => PdfDocument.Open(file))); merged.Save("merged.pdf"); // shared fonts and images are written once Console.WriteLine($"merged.pdf: {merged.Pages.Count} pages"); ``` ```bash dotnet add package PdfNetStack ``` ## Variations - To insert pages at a given place instead, use `doc.Pages.Import(other.Pages, index)`. - Annotations travel with their pages. Links to pages imported in the same call are remapped. - Encrypted inputs open with `PdfDocument.Open(path, password)`. Pages can be copied out of them only when their permissions allow copying. ## Related - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/): import, reorder, rotate and remove pages - [Command line](https://www.jadev-corp.com/cli/): `pdfnetstack merge out.pdf a.pdf b.pdf c.pdf` - API: [PdfDocument](https://www.jadev-corp.com/api/pdfnetstack/pdfdocument/), [PdfPageCollection](https://www.jadev-corp.com/api/pdfnetstack/pdfpagecollection/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/merge-pdf-files/ --- # Password protect a PDF in C# > Encrypt a PDF with AES-256 in .NET, with a user password, an owner password and permissions such as print and copy. MIT licensed, pure C#. Available since 1.0. Encrypt a PDF so it needs a password to open, and limit what readers may do with it. ## Short answer Call `doc.Security.Encrypt` with a user password, an owner password and the permissions to grant, then save. The user password opens the file with those permissions; the owner password gives full access. New protection is AES-256 unless you pass `PdfEncryption.Aes128`. ```csharp title="recipe-password-protect-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument doc = PdfDocument.Open("report.pdf"); doc.Security.Encrypt( userPassword: "open-me", // needed to open the file ownerPassword: "owner-secret", // needed to change it permissions: PdfPermissions.Print | PdfPermissions.CopyContent); // AES-256 by default doc.Save("report-protected.pdf"); PdfDocument check = PdfDocument.Open("report-protected.pdf", password: "open-me"); Console.WriteLine($"Opened with the user password: {check.Pages.Count} pages, {check.Security.Permissions}"); ``` ```bash dotnet add package PdfNetStack ``` ## Notes Permissions are `Print`, `PrintHighQuality`, `Modify`, `CopyContent`, `Annotate`, `FillForms`, `ExtractForAccessibility` and `Assemble`, combined with `|`. A document opened with only the user password can be read but not saved. One opened with the owner password keeps its passwords and permissions when saved. ## Related - [Encryption](https://www.jadev-corp.com/guides/encryption/) - [Remove a password from a PDF](https://www.jadev-corp.com/recipes/remove-pdf-password/) - [Command line](https://www.jadev-corp.com/cli/): `pdfnetstack encrypt in.pdf out.pdf --user-password u --owner-password o --allow print,copy` - API: [PdfSecurity](https://www.jadev-corp.com/api/pdfnetstack/pdfsecurity/), [PdfPermissions](https://www.jadev-corp.com/api/pdfnetstack/pdfpermissions/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/password-protect-pdf/ --- # Create a Factur-X invoice PDF in C# > Make a Factur-X or ZUGFeRD e-invoice in .NET, a PDF/A-3 file with the invoice XML attached and described in its metadata. MIT licensed, pure C#. Available since 1.1. Turn an invoice PDF and its XML into one Factur-X or ZUGFeRD hybrid invoice. ## Short answer Draw or convert the invoice, attach its XML with `PdfInvoice.Attach` and the profile it follows, then save as PDF/A-3b. The XML is checked for well formedness and embedded as `factur-x.xml` with the relationship the profile calls for, and saving writes the Factur-X XMP metadata with its extension schema. ```csharp PdfFont font = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2"); // PDF/A needs embedded fonts PdfDocument doc = PdfDocument.Create(); doc.Info.Title = "Invoice 42"; doc.Language = "en"; PdfPage page = doc.Pages.Add(PdfRectangle.A4); using (PdfGraphics g = page.CreateGraphics()) { g.DrawString("Invoice 42", font, 24, PdfColor.Black, 72, 100); g.DrawString("Total: 1,210.00 EUR", font, 12, PdfColor.Black, 72, 130); } // The invoice XML from your billing system, in Cross Industry Invoice syntax. byte[] xml = File.ReadAllBytes("invoice-42.xml"); PdfInvoice.Attach(doc, xml, PdfInvoiceProfile.EN16931); // embedded as factur-x.xml doc.Save("invoice-42.pdf", new PdfSaveOptions { Conformance = PdfConformance.PdfA3B }); PdfDocument check = PdfDocument.Open("invoice-42.pdf"); Console.WriteLine(string.Join(", ", check.Attachments.Select(a => a.Name))); ``` ## Notes - PDF/A needs embedded fonts, so the sample draws with an OpenType font rather than one of the standard 14. An invoice made with [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) works the same way when its fonts are embedded. - Profiles: `Minimum`, `BasicWL`, `Basic`, `EN16931`, `Extended` and `XRechnung`. PdfNetStack does not validate the XML against the profile's rules; check the invoice data before you attach it. - To sign the invoice, sign the saved bytes last, see [Signatures](https://www.jadev-corp.com/guides/signatures/). ## Related - [PDF/A](https://www.jadev-corp.com/guides/pdf-a/): levels, checks and the invoice options - [Convert an HTML invoice to PDF](https://www.jadev-corp.com/recipes/html-invoice-to-pdf/) - API: [PdfInvoice](https://www.jadev-corp.com/api/pdfnetstack/pdfinvoice/), [PdfInvoiceProfile](https://www.jadev-corp.com/api/pdfnetstack/pdfinvoiceprofile/), [PdfSaveOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfsaveoptions/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/pdf-a-3-xml-attachment/ --- # Reduce PDF size for email in C# > Make a PDF small enough to email in .NET by downsampling oversized images and storing photographs as JPEG, every output verified. MIT licensed. Available since 1.1. Shrink a PDF further than lossless compression allows, by lowering the resolution and quality of its photographs. ## Short answer Set the lossy image options on `PdfOptimizerOptions` and optimize as usual. `MaxImageDpi` downsamples images drawn sharper than the page needs, `ConvertImagesToJpeg` stores raw photographs as JPEG, and `JpegQuality` sets the quality of new JPEGs and requantizes existing ones. Text, vectors, fonts, line art and screenshots are untouched, and the output is still verified against the source. ```csharp var options = new PdfOptimizerOptions { MaxImageDpi = 150, // no image sharper than the page needs ConvertImagesToJpeg = true, // photographs stored raw become JPEG JpegQuality = 80, // new JPEGs, and existing JPEGs requantized down to it }; PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("brochure.pdf", "brochure-email.pdf", options); Console.WriteLine($"{result.Disposition}: {result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes, {result.SavingPercent:F1}% saved"); Console.WriteLine(result.Reason); ``` On the sample brochure, a lossless 1600 pixel photograph drawn about 6 inches wide, this takes the file from 2,452,280 to 62,318 bytes. ## Variations - Add `MinImageSsim = 0.95` to keep any image whose change would be visible; that image then keeps its own encoding, or stays raw after downsampling. - Version 1.0 on NuGet has `JpegQuality` only: it requantizes existing JPEG images and does nothing else lossy. - From the command line: `pdfnetstack optimize in.pdf out.pdf --max-dpi 150 --to-jpeg --jpeg-quality 80`. ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/#lossy-image-options): every lossy option and what the proof checks - [Downsample images in a PDF](https://www.jadev-corp.com/recipes/downsample-pdf-images/) - [Command line](https://www.jadev-corp.com/cli/) - API: [PdfOptimizerOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizeroptions/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/reduce-pdf-size/ --- # Remove a password from a PDF in C# > Decrypt a password protected PDF in .NET with its owner password and save it unprotected. Reads RC4, AES-128 and AES-256. MIT licensed. Available since 1.0. Save an unprotected copy of an encrypted PDF you have the owner password for. ## Short answer Open the file with its owner password, call `Security.RemoveEncryption`, and save. PdfNetStack opens every standard security handler revision: RC4 40 to 128 bit, AES-128 and AES-256. ```csharp PdfDocument doc = PdfDocument.Open("report-protected.pdf", password: "owner-secret"); doc.Security.RemoveEncryption(); doc.Save("report-unprotected.pdf"); ``` ```bash dotnet add package PdfNetStack ``` ## Notes The owner password is required: a document opened with only the user password can be read but not saved. A wrong password raises `PdfPasswordException`. ## Related - [Encryption](https://www.jadev-corp.com/guides/encryption/) - [Password protect a PDF](https://www.jadev-corp.com/recipes/password-protect-pdf/) - [Command line](https://www.jadev-corp.com/cli/): `pdfnetstack decrypt in.pdf out.pdf --password o` - API: [PdfSecurity](https://www.jadev-corp.com/api/pdfnetstack/pdfsecurity/), [PdfPasswordException](https://www.jadev-corp.com/api/pdfnetstack/pdfpasswordexception/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/remove-pdf-password/ --- # Rotate PDF pages in C# > Rotate one page or every page of a PDF in .NET by a multiple of 90 degrees, without rewriting the page content. MIT licensed, pure C#. Available since 1.0. Turn pages clockwise by 90, 180 or 270 degrees. ## Short answer Call `Rotate` on a page with a multiple of 90. The rotation adds to the page's current one and viewers apply it when they display the page; the content itself is not rewritten. `Rotation` reads the result: 0, 90, 180 or 270. ```csharp title="recipe-rotate-pdf-pages.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument doc = PdfDocument.Open("report.pdf"); doc.Pages[0].Rotate(90); // clockwise, a multiple of 90 foreach (PdfPage page in doc.Pages.Skip(1)) { page.Rotate(180); } doc.Save("report-rotated.pdf"); Console.WriteLine(string.Join(", ", doc.Pages.Select(p => p.Rotation))); ``` ```bash dotnet add package PdfNetStack ``` ## Related - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/) - [Command line](https://www.jadev-corp.com/cli/): `pdfnetstack rotate in.pdf out.pdf 90 2-4` - API: [PdfPage](https://www.jadev-corp.com/api/pdfnetstack/pdfpage/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/rotate-pdf-pages/ --- # Split a PDF into one file per page in C# > Split a PDF into single page files in .NET with PdfDocument.Split, each saved as a valid, optimized PDF. MIT licensed, pure C#. Available since 1.0. Turn every page of a PDF into a file of its own. ## Short answer `PdfDocument.Split` returns one new document per page. Save each one under its own name. Inherited page attributes such as resources, media box and rotation are materialized on each page, so every file stands on its own. ```csharp title="recipe-split-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument doc = PdfDocument.Open("report.pdf"); int number = 1; foreach (PdfDocument single in doc.Split()) // one new document per page { single.Save($"report-page-{number}.pdf"); number++; } Console.WriteLine($"Wrote {number - 1} files"); ``` ```bash dotnet add package PdfNetStack ``` ## Variations To keep groups of pages together rather than single pages, import the pages you want into a new document, as in [Extract pages from a PDF](https://www.jadev-corp.com/recipes/extract-pdf-pages/). ## Related - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/) - [Command line](https://www.jadev-corp.com/cli/): `pdfnetstack split in.pdf ./pages` - API: [PdfDocument](https://www.jadev-corp.com/api/pdfnetstack/pdfdocument/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/split-pdf/ --- # Convert a TIFF to PDF in C# > Turn a single or multi-page TIFF into a PDF in .NET, one page per image at its recorded resolution, fax pages kept compact. MIT, pure C#. Available since 1.1. Put every page of a TIFF file, such as a scan or a fax, on its own PDF page. ## Short answer `PdfImage.GetPageCount` counts the pages of the TIFF and `PdfImage.Load(path, page)` loads one, from 0. Add a PDF page the size of the image, which is its pixel size at the resolution the file records, and draw the image at its natural size. ```csharp title="recipe-tiff-to-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; PdfDocument doc = PdfDocument.Create(); int pages = PdfImage.GetPageCount("scan.tif"); for (int i = 0; i < pages; i++) { PdfImage image = PdfImage.Load("scan.tif", i); // A page the size of the image at its recorded resolution, in points. PdfPage page = doc.Pages.Add(PdfRectangle.FromSize(image.Width, image.Height)); using PdfGraphics g = page.CreateGraphics(); g.DrawImage(image, 0, 0); } doc.Save("scan.pdf"); Console.WriteLine($"{pages} pages written"); ``` ## Notes - A TIFF page stored as one strip of CCITT fax or JPEG data goes into the PDF as is, without decoding (CCITTFaxDecode, DCTDecode). Other pages are decoded and Flate compressed. - TIFF strips and tiles, chunky and planar data, PackBits, LZW, Deflate, CCITT Group 3 and 4 and JPEG compression, bilevel to 16 bit samples, alpha and BigTIFF are read. A damaged file raises a `FormatException`. - A file without a resolution is taken at 96 pixels per inch. To fit pages to A4 instead, add `PdfRectangle.A4` pages and draw with `PdfImageFit.Contain`. ## Related - [Images](https://www.jadev-corp.com/guides/images/): every supported format, fit and alignment - [Make a scanned PDF searchable](https://www.jadev-corp.com/recipes/make-scanned-pdf-searchable/) - API: [PdfImage](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfimage/), [PdfGraphics](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfgraphics/) Source: https://www.jadev-corp.com/pdfnetstack/recipes/tiff-to-pdf/ --- # PDF command line tool for .NET > Compress, merge, split, rotate, encrypt and inspect PDF files from the command line on Windows, Linux and macOS with pdfnetstack. MIT, no Ghostscript. Available since 1.0. `pdfnetstack` compresses, edits and protects PDF files from a terminal or a script, with the same engine and the same equivalence proof as the library. ## Short answer Install the tool once, then run a command per job. `optimize` is lossless by default and returns the original bytes when a rewrite would not be verified equivalent or would save less than 1%. ```bash dotnet tool install -g PdfNetStack.Tool pdfnetstack optimize in.pdf out.pdf pdfnetstack batch ./incoming ./optimized --report report.csv ``` It needs the .NET 8 or later runtime. `pdfnetstack --help` prints every command and option. ## Commands at a glance | Command | Does | |---|---| | [`optimize`](#optimize) | Compresses one file | | [`batch`](#batch) | Compresses or measures every PDF in a folder tree | | [`inspect`](#inspect) | Shows where the bytes of a file go | | [`verify`](#verify) | Checks that two files are equivalent | | [`merge`](#merge) | Joins files | | [`split`](#split) | Writes one file per page | | [`extract`](#extract) | Copies a page range into a new file | | [`rotate`](#rotate) | Rotates pages | | [`meta`](#meta) | Reads or writes the title, author, subject and keywords | | [`encrypt`](#encrypt) | Adds password protection | | [`decrypt`](#decrypt) | Removes password protection | | [`ocr`](#ocr) | Makes scanned pages searchable (separate package, 1.1) | ## optimize ```bash pdfnetstack optimize [options] pdfnetstack optimize in.pdf out.pdf --jpeg-quality 60 # lossy, JPEG images only pdfnetstack optimize in.pdf out.pdf --max-dpi 150 --to-jpeg --jpeg-quality 75 --min-ssim 0.95 pdfnetstack optimize in.pdf out.pdf --preset balanced # much smaller, no visible change pdfnetstack optimize in.pdf out.pdf --quantize # drawings and screenshots to a palette pdfnetstack optimize in.pdf out.pdf --exhaustive # archival: smaller Flate, much slower pdfnetstack optimize in.pdf out.pdf --linearize # fast web view pdfnetstack optimize huge.pdf out.pdf --streaming # bounded memory, any input size pdfnetstack optimize in.pdf out.pdf --json # the full result as JSON ``` The lossy image options (`--max-dpi`, `--to-jpeg`, `--quantize`, `--min-ssim`), `--preset`, `--progressive`, `--exhaustive`, `--linearize`, `--streaming` and the structural passes with their `--no-prune`, `--no-compact` and `--no-font-merge` switches arrived in 1.1; the 1.0.0 tool does not have them. The reason line names every lossy change. Prints the disposition, the sizes, the saving and the time, the reason, and for an optimized file the object counts and the time per phase. The output file is always written: the verified rewrite, or the input unchanged. Input and output must be different files. The library equivalent is [`PdfOptimizer.OptimizeFileAsync`](https://www.jadev-corp.com/guides/compress-pdf/). ## batch ```bash pdfnetstack batch [output-dir] [options] pdfnetstack batch ./incoming ./optimized --report report.csv pdfnetstack batch ./corpus # measure only, writes nothing ``` Finds every `.pdf` under the input folder, subfolders included, and writes each result to the same relative path under the output folder. Without an output folder nothing is written: the run measures what the optimizer would save. Documents run in parallel (`--jobs`, default all cores), each on one thread unless `--threads` is given. The summary lists documents optimized, retained and unsupported (grouped by reason), errors, the total size before and after, throughput and peak memory, and any verification failures, for which the original was kept. `--report ` writes one row per document with the columns `file`, `disposition`, `original_bytes`, `output_bytes`, `pages`, `milliseconds` and `reason`. A value that starts with a formula character is prefixed with a quote, so spreadsheet applications never evaluate it. `--json` prints the summary as JSON. ## inspect ```bash pdfnetstack inspect [--json] ``` Shows the size, PDF version, page count, objects (and how many are reachable), revisions and linearization, the cross-reference format, whether the file needed repair, is encrypted, signed or PDF/A-1, then the streams grouped by kind with their share of the file, and the filters used. The library equivalent is `PdfInspector.Inspect`. ## verify ```bash pdfnetstack verify pdfnetstack verify original.pdf other.pdf --jpeg-quality 60 # accept JPEG requantization pdfnetstack verify original.pdf other.pdf --min-ssim 0.95 # accept downsampled and converted images (1.1) ``` Runs the equivalence check the optimizer runs on itself, on the output of any tool. Prints `Equivalent.` and exits 0, or `Different:` with the first difference and exits 1. With `--jpeg-quality`, JPEG images may also differ by exactly the requantization the optimizer applies. With `--min-ssim`, downsampled and converted images are accepted when they keep their components and aspect, are no larger, keep their mean colors and reach that SSIM against the original resampled to their size; requantized images must then reach it too. See [Fidelity and verification](https://www.jadev-corp.com/concepts/fidelity/). ## merge ```bash pdfnetstack merge ... pdfnetstack merge out.pdf a.pdf b.pdf c.pdf ``` The output comes first, then the inputs in order. ## split ```bash pdfnetstack split ``` Writes one file per page, named after the input with a zero padded page number, such as `report-01.pdf` to `report-12.pdf`. ## extract ```bash pdfnetstack extract pdfnetstack extract in.pdf out.pdf 1-3,7,9- ``` Pages are numbered from 1. Ranges are separated by commas: `9-` runs to the last page, `-3` starts at the first, and `5-2` runs backwards. The order given is kept and pages may repeat. ## rotate ```bash pdfnetstack rotate [pages] pdfnetstack rotate in.pdf out.pdf 90 2-4 ``` Rotates clockwise by a multiple of 90 degrees, every page or the given ranges. ## meta ```bash pdfnetstack meta # print pdfnetstack meta in.pdf out.pdf --title "Annual report" --author "Finance" ``` Without an output file, prints the page count, every document information entry and the number of top level bookmarks. Text from the document is printed with control and bidirectional override characters escaped, so a crafted title cannot drive the terminal. With an output file, sets any of `--title`, `--author`, `--subject` and `--keywords` and saves. ## encrypt ```bash pdfnetstack encrypt --user-password u --owner-password o [--allow print,copy,modify,annotate,forms,accessibility,assemble,print-hq] [--aes128] ``` Writes AES-256 by default, AES-128 with `--aes128`. At least one password is required; an empty owner password is replaced by a random one. Without `--allow` every permission is granted; `--allow none` grants none, and `print-hq` includes `print`. See [Encryption](https://www.jadev-corp.com/guides/encryption/). ## decrypt ```bash pdfnetstack decrypt --password ``` Saves the document without protection. It needs the owner password. ## ocr ```bash pdfnetstack ocr [--languages latin,arabic] [--models ] [options] pdfnetstack-ocr [options] # the same tool, run directly ``` In the main branch, ships with 1.1. Makes scanned pages searchable: the scan on each page is read and its words are laid over it as invisible text, as the [OCR guide](https://www.jadev-corp.com/guides/ocr/) describes. OCR needs ONNX Runtime's native libraries and about 21 MB of models, so it is a tool of its own, `pdfnetstack-ocr`, in the `PdfNetStack.Ocr.Tool` package (about 70 MB, models included). `pdfnetstack ocr` runs it with the same arguments, found next to `pdfnetstack` or on the `PATH`, and says how to install it when it is missing: ```bash dotnet tool install -g PdfNetStack.Ocr.Tool ``` | Option | Effect | Library option | |---|---|---| | `--languages ` | Scripts to read, comma separated: `latin`, `arabic` (default `latin`) | `Languages` | | `--models ` | Load the models from this folder instead of the ones installed with the tool | `OnnxOcrEngine.Load(folder)` | | `--min-confidence <0-1>` | Leave out lines read with less confidence (default 0.5) | `MinimumConfidence` | | `--no-orientation` | Do not look for pages scanned sideways or upside down | `DetectOrientation = false` | | `--all-pages` | Also read pages that already show text | `SkipPagesWithText = false` | | `--threads ` | Pages recognized at once (default: all cores) | `MaxDegreeOfParallelism` | | `--text ` | Also write the text found, pages apart by a blank line | `PdfOcrReport.Text` | | `--password ` | Open an encrypted input | | | `--no-optimize` | Save without the optimizer pass | | | `--json` | Per page report: status, reason, orientation, lines and text | | Prints one line per page (lines and words read, how far a sideways page was turned, or why it was skipped) and the output size. The output is written when the run completes, even if some pages failed; it exits 1 in that case. Pages that already show text are left as they are, so a second run changes nothing. ## Options Optimizer options apply to `optimize`, `batch` and, where they make sense, to `verify`. | Option | Effect | Library option | |---|---|---| | `--fast`, `--balanced` | Faster Flate levels (default: maximum) | `Compression` | | `--exhaustive` | 1.1. Exhaustive Flate search for archival jobs: a few percent smaller, many times slower | `Compression = Exhaustive` | | `--no-object-streams` | Classic xref table, no object streams | `UseObjectStreams = false` | | `--no-dedup` | Do not merge identical objects | `DeduplicateObjects = false` | | `--no-prune` | 1.1. Keep resources no content uses | `PruneUnusedResources = false` | | `--no-compact` | 1.1. Do not rewrite content streams compactly | `CompactContentStreams = false` | | `--no-font-merge` | 1.1. Do not merge subsets of one TrueType font | `MergeFontSubsets = false` | | `--linearize` | 1.1. Write a linearized file (fast web view) | `Linearize` | | `--streaming` | 1.1. Read the input on demand and write the rewrite through a temporary file, proven there; no input size limit | `Streaming`, `MaxInputBytes` | | `--no-recompress` | Do not re-encode streams | `RecompressStreams = false` | | `--no-image-predictors` | Do not try PNG prediction on raw images | `UseImagePredictors = false` | | `--no-jpeg` | Do not re-encode JPEG images | `OptimizeJpegImages = false` | | `--jpeg-quality <1-100>` | Requantize JPEG images, and the quality of new JPEGs (lossy) | `JpegQuality` | | `--max-dpi ` | 1.1. Downsample images drawn above 1.5 times n dpi to n (lossy) | `MaxImageDpi` | | `--max-bilevel-dpi ` | 1.2. The same for bilevel images, in place of `--max-dpi` (lossy) | `MaxBilevelImageDpi` | | `--to-jpeg` | 1.1. Re-encode photographic raw images as JPEG at `--jpeg-quality` (lossy) | `ConvertImagesToJpeg` | | `--quantize` | 1.1. Reduce raw RGB images to a palette when that stays faithful (lossy) | `QuantizeImages` | | `--preset ` | 1.1. A ready set of lossy options; options after it change it | `PdfOptimizerOptions.Balanced`, `Smallest` | | `--min-ssim <0-1>` | 1.1. Keep any image whose lossy change falls below this SSIM | `MinImageSsim` | | `--progressive` | 1.1. Write JPEG progressively when that is smaller (lossless) | `ProgressiveJpeg` | | `--no-verify` | Skip the equivalence proof (not recommended) | `Verify = false` | | `--allow-incremental` | Flatten files with incremental updates | `AllowIncrementalUpdates` | | `--allow-repaired` | Rewrite damaged files that needed repair | `AllowRepairedFiles` | | `--strip-thumbnails` | Remove page thumbnails (not lossless) | `RemovePageThumbnails` | | `--strip-pieceinfo` | Remove application private data (not lossless) | `RemovePieceInfo` | | `--min-saving ` | Minimum saving to keep a rewrite (default 1) | `MinSavingPercent` | | `--threads ` | Threads per document (default: all cores) | `MaxDegreeOfParallelism` | | `--jobs ` | Documents in parallel for `batch` (default: all cores) | | | `--report ` | `batch`: one row per document | | | `--json` | Machine readable output | | The editing commands (`merge`, `split`, `extract`, `rotate`, `meta`, `encrypt`, `decrypt`) save through the lossless optimizer; `--no-optimize` skips that pass, and `--fast` or `--balanced` set the Flate level. Every editing command accepts `--password` to open an encrypted input. ## Full help text The output of `pdfnetstack --help`, captured from the tool built from the same source as this page: ```text pdfnetstack: lossless PDF optimizer Usage: pdfnetstack optimize [options] pdfnetstack batch [output-dir] [options] (no output-dir: measure only) pdfnetstack inspect [--json] pdfnetstack verify Editing: pdfnetstack merge ... pdfnetstack split pdfnetstack extract e.g. 1-3,7,9- (order kept, repeats allowed) pdfnetstack rotate [pages] pdfnetstack meta [output.pdf] [--title t] [--author a] [--subject s] [--keywords k] Security: pdfnetstack encrypt --user-password u --owner-password o [--allow print,copy,modify,annotate,forms,accessibility,assemble,print-hq] [--aes128] pdfnetstack decrypt --password Every editing command accepts --password to open an encrypted input. OCR (needs the PdfNetStack.Ocr.Tool package; pdfnetstack-ocr --help for its options): pdfnetstack ocr [--languages latin,arabic] [--models ] Options: --fast | --balanced Faster Flate levels (default: maximum) --exhaustive Exhaustive Flate search for archival jobs: a few percent smaller, many times slower --no-object-streams Write a classic xref table, no object streams --no-dedup Do not merge identical objects --no-prune Keep resources no content uses --no-compact Do not rewrite content streams compactly --no-font-merge Do not merge subsets of one TrueType font --linearize Write a linearized file (fast web view) --streaming Read the input on demand and write the rewrite through a temporary file, proven there: memory stays bounded for files larger than memory, and inputs of any size are accepted --no-recompress Do not re-encode streams --no-group4 Do not re-encode bilevel images as CCITT Group 4 --no-image-predictors Do not try PNG prediction on raw images --no-jpeg Do not re-encode JPEG images --jpeg-quality <1-100> Requantize JPEG images to this quality (lossy); verify: accept images requantized this way --max-dpi Downsample images drawn above 1.5 times n dpi to n (lossy) --max-bilevel-dpi The same for bilevel images, in place of --max-dpi (lossy) --to-jpeg Re-encode photographic raw and JPEG 2000 images as JPEG at --jpeg-quality (lossy) --quantize Reduce raw RGB images to a palette when that stays faithful: drawings and screenshots (lossy) --preset balanced: palette, JPEG 75, SSIM at least 0.95. smallest: palette, JPEG 50, 150 dpi, SSIM at least 0.90. Both lossy; options after it change it --min-ssim <0-1> Perceptual gate: keep any image whose lossy change falls below this SSIM; verify: accept downsampled and converted images that meet it --progressive Write JPEG progressively when that is smaller --no-verify Skip the equivalence proof (not recommended) --allow-incremental Flatten files with incremental updates --allow-repaired Rewrite damaged files that needed repair --strip-thumbnails Remove page thumbnails (not lossless) --strip-pieceinfo Remove application private data (not lossless) --min-saving Minimum saving to keep a rewrite (default 1) --threads Threads per document (default: all cores) --jobs Documents processed in parallel by batch (default: all cores) --report Batch: write one row per document --json Machine readable output --no-optimize Editing: save without the optimizer pass ``` ## Exit codes | Code | Meaning | |---|---| | 0 | Success. For `verify`: the files are equivalent | | 1 | `verify`: the files differ. `ocr`: the output was written, but some pages failed | | 2 | Wrong arguments, an unreadable PDF, a wrong password, or a file error. The message goes to standard error | An `optimize` or `batch` run that keeps the original, because the saving is too small or the file is refused by the [safety model](https://www.jadev-corp.com/concepts/safety-model/), is a success: the output is a valid PDF. ## Limits - The tool works on files only; it has no stdin or stdout mode. - Lossy options handle 8 bit gray, RGB and CMYK images, raw or JPEG; `--to-jpeg` needs `--jpeg-quality`. JPEG 2000, JBIG2, CCITT, 16 bit, indexed, Lab, Separation and DeviceN images keep their resolution. - By default the optimizer holds each document in memory while it processes it. For large image heavy files, lower `--threads` or `--jobs`, or use `--streaming`, which keeps memory bounded and needs room for the output in the temporary folder next to it. See [Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/). ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/): the optimizer from C# - [Linearize a PDF for fast web view](https://www.jadev-corp.com/recipes/linearize-pdf/) - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/): merge, split and extract from C# - [Benchmarks](https://www.jadev-corp.com/benchmarks/): how `batch` reports were used to measure a corpus - [OCR](https://www.jadev-corp.com/guides/ocr/): the same from C#, and what happens to each page - API: [PdfOptimizer](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizer/), [PdfOptimizerOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizeroptions/), [PdfInspector](https://www.jadev-corp.com/api/pdfnetstack/pdfinspector/), [PdfEquivalence](https://www.jadev-corp.com/api/pdfnetstack/pdfequivalence/) ## How do I install the pdfnetstack command? Run dotnet tool install -g PdfNetStack.Tool. It needs the .NET 8 or later runtime and works on Windows, Linux and macOS. ## Can pdfnetstack compress a whole folder of PDFs? Yes. pdfnetstack batch optimizes every PDF under the folder, subfolders included, and --report writes one CSV row per document. Without an output folder it only measures. Source: https://www.jadev-corp.com/pdfnetstack/cli/ --- # Architecture > How the PdfNetStack optimizer is built: the pipeline from parsing to verification, the source layout, design decisions and threading. > **Synced from the repository:** This page is generated from [docs/ARCHITECTURE.md](https://github.com/jams4code/pdf.netstack/blob/main/docs/ARCHITECTURE.md) at every build. PdfNetStack is a rewriting optimizer, not a PDF library. It understands enough of the file format to rebuild the object graph exactly, and nothing about rendering. That narrow scope is what keeps it small, fast and provable. ## Pipeline ``` input bytes | v PdfFile.Open ............ header, xref chain (tables, streams, hybrid), object streams, | reconstruction for damaged files; stream data sliced, not copied v Safety screen ........... encrypted, signed, incremental, damaged, too large -> return input | v ObjectGraph.Collect ..... breadth first from /Root and /Info; everything else is garbage; | marks identity sensitive objects; optional stripping v ContentAnalysis ......... reads all content once: names each resource dictionary is looked | up with, which streams are content only, glyphs each font shows v ResourcePruner .......... drops resource entries no content can name FontProgramMerger ....... replaces subsets of one TrueType font by their union InheritedAttributes ..... (Linearize) moves inherited page attributes onto the pages | v StreamRecompressor ...... parallel; decode compression layers, compact content streams, | try candidates, keep smallest; hash what each stream decodes to v Deduplicator ............ canonical serialization + SHA-256, streams by decoded content, | merged to a fixpoint v PdfDocumentWriter ....... renumber, pack object streams, write xref stream LinearizedWriter ........ (Linearize) Annex F layout, two xref sections, hint stream | v EquivalenceVerifier ..... re-open source and output, walk both graphs, compare decoded streams | v output = verified rewrite, or input bytes ``` ## Source layout | Folder | Responsibility | |---|---| | `Objects/` | Object model. Numbers keep canonical text so reals never pass through floating point. Dictionaries keep insertion order for deterministic output. | | `Parsing/` | Tokenizer, object parser, `PdfSource` (a buffer in memory or a seekable stream), `PdfFile` (xref loading, lazy object resolution, object streams, reconstruction). | | `Filters/` | Flate, LZW, ASCIIHex, ASCII85, RunLength decoders; PNG and TIFF predictors; pooled, size capped buffers; `ExhaustiveDeflate`, the opt-in Zopfli style encoder. | | `Imaging/` | JPEG at the coefficient level: marker parser, Huffman decoder for baseline, extended and progressive frames, Huffman code optimizer, baseline writer, requantization, and the coefficient equivalence check. On the read side, `JpegDecoder` (inverse DCT, fancy upsampling and color conversion as libjpeg does them), `CcittDecoder` (Group 3 and 4 fax), `Jpeg2000Decoder` (JP2 boxes, codestream, tier 2 packets, EBCOT with its MQ coder, wavelets, color transforms), `ImageSamples` (1 to 16 bit samples, Decode arrays, color spaces) and a PNG reader and writer. For image input, GIF, BMP and TIFF readers, and WebP: `Vp8Decoder` (lossy key frames, RFC 6386) and `Vp8LDecoder` (lossless, RFC 9649), both producing a `DecodedImage` that `PdfImage` embeds. For the opt-in lossy image options: `JpegEncoder` (forward DCT, libjpeg tables), progressive output in `JpegWriter`, `Resampler` (Lanczos), `Ssim` and `ImageClassifier`. Without those options the optimizer never decodes pixels. | | `Content/` | Content streams on the read side: `ContentReader` (operators, operands, inline images, tolerant of damage) and `ContentInterpreter` (graphics and text state, forms, marked content, bounded work), which image and text extraction build on. `ContentCompactor` rewrites a content stream as the same tokens in fewer bytes and checks two streams token by token; `InlineImageBounds` finds where inline image data ends, and says when readers could disagree. | | `Text/Extraction/` | Text extraction: `TextExtractor` (glyph placement from the text state, ActualText), `TextFont` (codes, Unicode and widths of simple, Type 3 and composite fonts), `CMap` (code spaces, CID and ToUnicode maps, usecmap), `PredefinedCMaps` (Adobe's CJK CMaps and CID to Unicode tables, Brotli packed resources generated by `tools/make_cjk_cmaps.py`, read lazily), `GlyphList` (Adobe Glyph List, generated by `tools/make_glyph_list.py`), `FontEncodings` (the predefined encodings) and `TextLayout` (lines, words, right to left order). | | `Fonts/` | Public font types, `PdfFontCollection`, and `FontMatcher`: the CSS Fonts 4 matching of width, style and weight over faces with ranges, and the variation values to apply, shared by collections and `@font-face` in the HTML package. | | `Fonts/OpenType/` | OpenType fonts: `OpenTypeFile` (sfnt, collections, WOFF, WOFF2), `CffFont` and `TrueTypeSubsetter` (embedded subsets), `Shaper` with `ShaperNormalization`, `IndicShaper`, `UseShaper`, `ThaiShaper`, `KhmerShaper`, `MyanmarShaper`, `HangulShaper` and `SyllableGrammar` (GSUB and GPOS, normalization, the script shapers and their syllable grammars), and `FontVariations`, `FontInstancer` and `Cff2Converter` (static instances of variable fonts). | | `Optimization/` | Reachability, content analysis, resource pruning, font program merging, recompression, deduplication, verification; `ImagePlacement` (largest drawn size of each image) and `LossyImages` (downsampling, JPEG conversion, the quality gate and its proof). `ReferenceIndex` records where every reference sits, so a pass can prove an object is reached only in the role it understands. | | `Documents/` | The editing model. `ObjectStore` holds a document's objects, or reads them from its file on demand. | | `Writing/` | Token serializer with minimal separators; document writer for compact and classic layouts, whole in memory or as a sequence of chunks; linearized writer; incremental update writer. | | `PdfOptimizer.cs` | Public entry point and orchestration. Every exception path returns the input. `PdfOptimizer.Streaming.cs` runs the same passes with the source on demand and the rewrite in a temporary file. | ## Design decisions **The optimizer reads everything into memory, by default.** PDFs are random access by design and every pass touches most of the file. A single buffer with `ReadOnlyMemory` slices is the simplest correct model and the fastest one. Stream data is never copied until it is re-encoded. **Streaming mode keeps the passes and the proof, not the data.** With `PdfOptimizerOptions.Streaming` the source is a `PdfFile` on a stream, so only object dictionaries and the cross-reference data stay in memory. Pruning, font merging and compaction change dictionaries and a few font programs, as in memory. Recompression is planned for every stream first (`StreamRecompressor.Prepare`) and done while writing: the writer asks for each stream in output order, and the streams from that one on are recompressed side by side up to 32 MiB of source data, handed over once and forgotten. Deduplication must be decided before the first object is written, so it keys each planned stream by a hash of what it decodes to through its general filters, plus what its plan may do to it (compact it, requantize it, resample it to which size); two streams with equal keys and equal dictionaries recompress to the same content. That costs one extra decode per stream and no encoding. The rewrite goes to a temporary file, and the proof is `EquivalenceVerifier` on a fresh reader over the source (`PdfFile.Fork`) and a reader over that file, both on demand: the same walk and the same stream comparisons as in memory, with each stream decoded while it is compared and then dropped. **Saving writes as it goes.** `PdfDocumentWriter` produces the file as a sequence of chunks: a 64 KB buffer of serialized objects, or stream data of that size or more passed on as it is, with offsets counted as the bytes go out and the cross-reference data last. In memory the same code keeps everything in one buffer, so `ToArray` and a progressive `Save` cannot drift apart. `SaveAsync` awaits each chunk's write. A save with the optimizer still builds the file in memory first, because the optimizer proves its rewrite against all of it. **Documents can be read on demand.** `PdfLoadMode.OnDemand` puts `PdfFile` on a seekable stream instead of a buffer, through the same `PdfSource` abstraction, with 64 bit offsets: the caller's stream, a file the document opened by path, or a `Spool`, the document's own copy of a stream that cannot seek (in memory up to a threshold, else a temporary file deleted on dispose). Disposing the document closes the source, so any later read throws `ObjectDisposedException`. Opening reads the header, the cross-reference data (checking each in-file entry's object header, 4 KB at a time) and the page tree. Any other object is parsed when first resolved and kept, so it stays the same instance; dictionaries are parsed from a window that grows only while the parse ends too close to its edge. Stream data is not read with the dictionary: each access to `PdfStream.Data` reads it from the stream again, and nothing keeps it, until new data is assigned. Decoded object streams share the usual cache, capped at one decode limit. Memory is therefore the objects the application has resolved plus the data it is holding, whatever the file size: reading the text of one page of a large scan reads that page's content and fonts, not its images. `SaveIncremental` writes only what changed after the original bytes, copying them a megabyte at a time, or appends to the source stream in place. A full save without the optimizer is written progressively, its stream data read from the source as it goes; `ToArray` and a save with the optimizer build the whole output in memory. On a synthetic 400 MB file of 100 image pages, opening on demand and extracting one page's text took 0.24 s and added 9 MB to the working set; the same with the file read into memory took 0.63 s and 387 MB. Parsed objects stay until `ReleaseUnusedObjects` drops those that still read as the file has them (each is parsed again and compared) and that nothing else references. The second test is a full garbage collection: the candidates are held only weakly across it, and any that survive are referenced from somewhere and return to the cache, so no instance anyone holds is ever replaced and no edit is lost. **Incremental saves compare, they do not track.** The document keeps no change log. On `SaveIncremental` a second reader parses the original objects afresh, and every object the document has resolved is serialized and compared with its original; stream data counts as changed once it was assigned. Inherited attributes copied onto pages when the document was opened are not a change. New objects are written when a written object, or the trailer, reaches them. **Content is analysed once, before anything changes.** Pruning, compaction and font merging all rest on one `ContentAnalysis` of the unmodified file. It errs one way only: a name anywhere in the bytes of a content stream counts as used, content it cannot decode keeps all of its resources, and a budget overrun turns pruning and font merging off. The verifier repeats the analysis on its own parse of the source rather than trusting the optimizer's. **Linearize the way qpdf checks it.** Annex F leaves room in how objects are grouped. The linearized writer assigns them to parts by who uses them exactly as qpdf does, so `qpdf --check-linearization` computes the same layout from the output. Hint table offsets are those of the file without the hint stream, so the hint stream is built after everything else is laid out and then inserted; the numbers in the linearization dictionary and first trailer are padded to a fixed width so their own size never moves an offset. **Numbers as canonical text.** `0.50` and `.5` are the same number, but `0.1 + 0.2` in floating point is not `0.3`. Keeping the digits avoids any chance of drift and still lets the writer emit the shortest form. **Recompression decodes only compression layers.** Predictor transforms stay in place under the new Flate layer, so a predicted image or xref stream is recompressed without reimplementing the predictor on the encode side. Candidate encodings are compared by total cost, data plus the `/Filter` and `/DecodeParms` entries they need. **JPEG stays in the frequency domain.** A JPEG is quantized DCT coefficients plus Huffman coding. Re-encoding the coding alone is lossless by definition, and requantizing the coefficients is the same loss a JPEG encoder applies at a lower quality, applied once instead of after a decode and a re-encode. Skipping pixels also means color conversion, chroma upsampling and the color model never enter the picture, so CMYK, YCCK, untransformed RGB and gray images keep their meaning without special cases, and the result is integer exact on every platform. **Fast trial, slow winner.** For images, plain and PNG predicted data are both compressed at the fastest zlib level, and only the smaller one is compressed at the configured level. Streams are scheduled largest first so one big image never runs alone at the end. **Identity before equality.** Two equal annotation dictionaries are still two annotations. The graph walk marks objects reached through identity keys (`/Parent`, `/Kids`, `/Annots`, `/P`, `/Pg`, `/Obj`, outline links, form fields and more), and the deduplicator also skips types such as pages, annotations and structure elements. Everything else is merged only when its canonical serialization, with references mapped through the merge table, is identical. **Pooled buffers for decoded data.** Decoders write into `ArrayPool` backed buffers that are handed out as leases and returned once consumed. On a 72 MiB drawing catalogue this took peak working set from 1.8 GB to under 700 MB. **Verification is a separate parse.** The optimizer mutates its object model as it works, so the verifier opens the original bytes again and compares two independent parses. A writer bug and a reader bug would have to cancel out exactly to slip through, and the independent PDFium render check in `tools/` covers that case. ## Threading Parsing and graph work run on one thread. Stream recompression and stream comparison run in parallel, after every object they need has been resolved, so workers never call back into the parser. `PdfFile.GetObject` is still locked as a guard. `MaxDegreeOfParallelism` caps both phases and is the main lever on peak memory. A stream source serializes its reads, so a document opened on demand can be read from several threads, though each read waits for the last. Static state is shared by every document in the process and kept bounded for long running hosts. Lookup tables are built once, lazily where only some inputs need them (the CJK CMaps, the syllable grammars of the complex script shapers), under locks or `LazyInitializer`, and their size depends only on data shipped with the library. Caches that grow with the input belong to a document or a conversion and go with it. Variable font instances are held weakly by the font that made them, so a font shared across requests keeps only the instances still in use. Source: https://www.jadev-corp.com/pdfnetstack/concepts/architecture/ --- # PDF coordinates and units in PdfNetStack > How PdfNetStack places things in points from the top left when you draw, and reports bottom left PDF user space when you extract text and images. Available since 1.1. Two coordinate systems, one unit: what each API uses, and how to convert between them. ## Short answer Everything is in points, 1/72 inch. When you create content (drawing, annotations, form fields), the origin is the top left corner of the page and y points down. When you read content back (text, images, annotation and widget rectangles), positions are in PDF default user space: origin bottom left, y up, before the page's `/Rotate`. ```csharp // One point is 1/72 inch. Convert once, draw in points. static double Mm(double millimetres) => millimetres * 72 / 25.4; static double Inches(double inches) => inches * 72; PdfDocument doc = PdfDocument.Create(); PdfPage page = doc.Pages.Add(PdfRectangle.A4); // 595 x 842 points, about 210 x 297 mm using (PdfGraphics g = page.CreateGraphics()) { // A box 20 mm from the left and 30 mm from the top: y grows downward on the canvas. g.DrawRectangle(PdfPen.Black, null, Mm(20), Mm(30), Mm(80), Mm(15)); g.DrawString("Hello", PdfStandardFont.Helvetica, 12, PdfColor.Black, Mm(20) + 6, Mm(30) + 26); g.DrawLine(PdfPen.Black, Inches(1), Inches(2), Inches(3), Inches(2)); } doc.Save("coordinates.pdf"); ``` ## Units | Unit | Points | |---|---| | 1 inch | 72 | | 1 cm | 28.35 (72 / 2.54) | | 1 mm | 2.835 (72 / 25.4) | | CSS pixel | 0.75 (96 per inch) | `PdfRectangle.A4` is 595 by 842 points (210 by 297 mm, rounded), `PdfRectangle.Letter` 612 by 792. `PdfRectangle.FromSize(width, height)` makes any other size, such as `FromSize(842, 595)` for A4 landscape. Images drawn at their natural size use the resolution the file records, else 96 pixels per inch. ## Creating content: top left, y down | API | Origin | |---|---| | `PdfPage.CreateGraphics()` and every `PdfGraphics` method | Top left corner of the media box, y down | | Every `page.Annotations.Add...` method: links, notes, markup, free text, stamps, shapes, lines, ink, attachments | Same as the canvas | | `doc.Form.AddTextField` and the other field methods | Same as the canvas | | `PdfForm.CreateGraphics()` | Top left corner of the form | | `PdfTextLayout` lines (`Top`, `Baseline`, `Left`) | Top left corner of the laid out paragraph | Two details matter when placing text: - `DrawString(text, font, size, brush, x, y)` puts the start of the baseline at (x, y). Add `font.Ascent * size` to y to place the top of the line instead. - `DrawText(paragraph, x, y, width)` puts the top left corner of the paragraph at (x, y). Canvas transforms (`TranslateTransform`, `RotateTransform` and the others) change where later drawing lands. Positive rotation angles turn clockwise on the canvas. `PdfGraphics.AddLink` follows the current transform; the annotation methods on `page.Annotations` do not. ## Reading content: bottom left, y up | API | Coordinates | |---|---| | `PdfPage.GetWords()`, `GetLetters()`: `Bounds`, `BaselineStart`, `BaselineEnd` | Default user space, before `/Rotate` | | `PdfPage.GetImages()`: `Bounds`, `Transform` | Default user space, before `/Rotate` | | `PdfAnnotation.Rectangle`, `PdfWidget.Rectangle` | PDF user space | | `PdfPage.MediaBox`, `CropBox` | PDF user space, as `PdfRectangle(Left, Bottom, Right, Top)` | ```csharp // Extraction reports default user space: origin bottom left, y up. PdfWord word = PdfDocument.Open("coordinates.pdf").Pages[0].GetWords()[0]; Console.WriteLine($"{word.Text}: left {word.Bounds.Left:F1}, bottom {word.Bounds.Bottom:F1}, top {word.Bounds.Top:F1}"); // Back to canvas coordinates: y from the top is the page height minus y from the bottom. double canvasTop = page.MediaBox.Top - word.Bounds.Top; Console.WriteLine($"On the canvas the word's top is {canvasTop:F1} points from the top"); ``` To convert a point from user space to the canvas: `x = pdfX - page.MediaBox.Left` and `y = page.MediaBox.Top - pdfY`. For a rectangle, its canvas top is `MediaBox.Top - rect.Top` and its height stays `rect.Height`. That is how you place a highlight over a word that text extraction found. ## Rotated pages `PdfPage.Rotation` is the clockwise rotation a viewer applies when it displays the page: 0, 90, 180 or 270. `Rotate(degrees)` adds to it. - The canvas does not apply `/Rotate`: coordinates are those of the unrotated page. On a page rotated by 90 degrees, what you draw at the canvas top shows on the right side in a viewer. - Extraction positions are also before `/Rotate`. - `PdfForm.FromPage` shows a page upright, as its rotation shows it, so placing a rotated page on another page needs no extra turn. ## Media box and crop box The media box is the page boundary; it defaults to US Letter when a file does not define one. The crop box is the visible region and defaults to the media box. The canvas origin is the top left of the media box, and `PdfForm.FromPage` shows the crop box. ## Related - [Drawing](https://www.jadev-corp.com/guides/drawing/) - [Extract text](https://www.jadev-corp.com/guides/extract-text/) - [Annotations](https://www.jadev-corp.com/guides/annotations/) - API: [PdfRectangle](https://www.jadev-corp.com/api/pdfnetstack/pdfrectangle/), [PdfPage](https://www.jadev-corp.com/api/pdfnetstack/pdfpage/), [PdfGraphics](https://www.jadev-corp.com/api/pdfnetstack-drawing/pdfgraphics/), [PdfWord](https://www.jadev-corp.com/api/pdfnetstack/pdfword/) Source: https://www.jadev-corp.com/pdfnetstack/concepts/coordinates-and-units/ --- # Fidelity and verification > How PdfNetStack proves an optimized PDF equivalent to its source: graph comparison, JPEG coefficient checks, render comparison and tests. > **Synced from the repository:** This page is generated from [docs/FIDELITY.md](https://github.com/jams4code/pdf.netstack/blob/main/docs/FIDELITY.md) at every build. "Lossless" here means a conforming PDF reader cannot tell the output from the input: same pages, same content, same fonts and images, same annotations, forms, bookmarks, structure, metadata and attachments. Three layers of evidence back that claim. ## 1. Built-in equivalence proof Runs on every optimization unless `Verify` is turned off. The source bytes and the candidate bytes are parsed independently. Starting at the trailer's `/Root`, `/Info` and `/ID`, the verifier walks both graphs together: - names, strings, booleans and numbers must be identical (numbers compared by canonical value), - arrays must have the same length and matching items, - dictionaries must have the same keys with matching values; an entry whose value is null counts as absent, as the specification defines, - references are followed in pairs, so cycles terminate; a source object must always map to the same output object, and two source objects may share one output object only when neither has identity of its own (annotations, form fields, pages, structure elements and tree nodes never do), - every stream is decoded through all decodable filters and predictors on both sides and the bytes must match exactly; filters that stay encoded (DCT, JPX, JBIG2, CCITT) must be identical, including their parameters and any objects they reference such as `/JBIG2Globals`, - JPEG data whose bytes changed is decoded to DCT coefficients on both sides (see below), - a bilevel image whose output is CCITT data is decoded to pixels on both sides (see below), - streams that cannot be decoded must be byte identical with an identical filter chain. Apart from the structural changes below, bilevel images coded again, and the opt-in image changes described under JPEG images, the only accepted differences are the encoding entries of streams: `/Length`, `/Filter`, `/DecodeParms` and `/DL`. With opt-in stripping enabled, `/Thumb` and `/PieceInfo` may be missing from the output. ### Structural changes The lossless structural passes change more than encoding, so each has a rule of its own. None is taken on trust: the verifier runs its own content analysis on its own parse of the source, and accepts a difference only where that analysis proves it invisible. `PdfEquivalence` and `pdfnetstack verify` apply the same rules. **Unused resources.** A resource entry (font, XObject, graphics state, pattern, shading, color space, property list) may be missing from the output only when no content that can use it names it. The analysis reads every content stream from the page tree and from annotation appearances (every state of /N, /R and /D), and follows form XObjects, tiling patterns, Type 3 glyph procedures, soft mask groups and fonts selected through graphics states. A name counts as used when it appears anywhere in the decoded content as a name token, whatever operator it belongs to, and even inside strings, comments or inline image data. A page's content parts are scanned joined with and without a separator between them, as readers differ. Content with resources of its own that names something those resources lack also keeps that name in the resources of the content that invoked it, since some readers look there. `DefaultGray`, `DefaultRGB` and `DefaultCMYK` are used implicitly and always kept. A category dictionary is pruned only when every resource dictionary holding it belongs to content the analysis read in full: a dictionary also reached as form default resources (`/DR`), from content that does not decode, from a holder that was never executed, or through any other key keeps every entry. When a work budget runs out (2 GB of decoded content or 50 million operators) nothing is pruned. **Content streams.** A stream that is only ever used as content (page contents, forms, tiling patterns, appearances, glyph procedures, soft mask groups, widget icons) may be rewritten when it reads as the same tokens. The comparison tokenizes both versions: operators must be byte identical, numbers equal by canonical value, names equal after `#xx` decoding, strings equal by their bytes whether written literal or hex, and inline image dictionaries equal by value with their data byte identical. Inline image data whose end is ambiguous (a filtered image without /L whose data contains `EI`, or rules that disagree) stops the rewrite: everything from that image on must be byte identical. Because readers join a page's parts in different ways, a leading or trailing white space byte must be kept, and a first or last token that touches the edge must keep its exact bytes. A stream that ends inside a comment is not changed. **Merged font programs.** The TrueType program behind a composite font may be replaced by a program shared with other subsets of the same font when: the font is Type0 with Identity-H or Identity-V, its descendant is a CIDFontType2 with an identity /CIDToGIDMap and no /CIDSet, the program is only reached through such fonts, and the analysis decoded every string shown with them. The glyphs a program must keep are the codes those fonts show, glyph 0 and every component of a composite glyph among them. For a merged program the verifier requires: every table other than glyf, loca, hmtx, hhea, maxp, head, post, name and cmap byte identical; the same version, revision, flags, units per em, style, recommended size, direction hint and glyph data format in head; the same ascender, descender, line gap, caret slope and metric format in hhea; the same maxp version and hinting limits at least as wide; and for each of those glyphs the same outline bytes (up to trailing zero padding), the same advance width and the same left side bearing. Glyph names (post) are dropped and the name and cmap tables come from one of the merged programs; a CIDFontType2 with an identity map selects glyphs by id and reads none of them. **Streams equal after decoding.** Deduplication compares streams by what they decode to, so two copies of a font program or image merge even when they were encoded differently. The comparison covers the decoded bytes, any predictor and filter still applied, and the stream dictionary without its encoding entries. **Inherited page attributes.** With `Linearize`, `/Resources`, `/MediaBox`, `/CropBox` and `/Rotate` move from intermediate page tree nodes onto the pages that inherit them. The verifier then compares each page by the value it ends up with, its own or its nearest ancestor's, and does not compare those keys on intermediate nodes, whose values matter only through the pages. ### Streaming mode With `PdfOptimizerOptions.Streaming` the proof is the same one, not a sampled or per page check. The rewrite is written to a temporary file, and the verifier runs on two readers that load objects on demand: a fresh reader over the source, which has parsed nothing the passes touched, and a reader over that file. It walks both graphs from the trailer and compares every reachable object, and every stream by its decoded content, under the same rules as above. Each stream is decoded only while it is compared. The output must also parse with no repaired stream length, checked again after the walk has read every stream. A merged object is allowed exactly the image change its representative got, which is what the output holds for it. Only a proven rewrite reaches the destination; otherwise the input is copied there unchanged. ### Linearized output A linearized file holds the same objects as any other rewrite, so the proof above covers its content. Its layout is checked separately. The tests read every linearized file back and check the linearization dictionary against the file (`/L`, `/O`, `/E`, `/N`, `/T`, `/H`), both cross-reference sections, and every entry of the page offset, shared object and outline hint tables against the real offsets and lengths of the objects. `tools/measure_optimizer.py --linearize` also runs qpdf's `--check-linearization` on each output when qpdf, or pikepdf with its bundled libqpdf, is installed. The objects are assigned to the parts of Annex F the way qpdf assigns them, so its check computes the same layout. ### Bilevel images The Group 4 pass (`OptimizeBilevelImages`) codes 1 bit images again as CCITT Group 4: stencil masks and 1 bit gray, palette and single colorant images, stored as samples under any general filters (predictors included) or as CCITT Group 3. The encoder follows ITU-T T.6 and chooses its modes by the coding procedure of T.4, so it writes the same bytes as libtiff (a test checks this on libtiff's own output). /BlackIs1 is set when the 0 bits are the more common value, which keeps the samples as they were while long runs use the short white codes. The proof accepts an output coded as CCITT data in place of samples or CCITT data only by its pixels. Both sides are decoded, every pixel of every row must match, and the padding bits at the end of a row, which are not pixels, are ignored. Readers recover from flawed fax data in different ways, so only data every reader takes alike counts, on both sides: - parameters of their own type (integers, booleans), /Columns equal to the width (left out it means 1728 to some readers and the width to others), /Rows unset or equal to the height, - data that decodes without a flaw: every code exists, no run overshoots its row, the changes on a row are strictly ordered (an empty run only where a row starts black or ends), no pass reaches past the reference row, and every row is there before the data ends or stops, - samples at least as long as the image. Anything else must match as it was, encoded bytes and parameters included. Group 3 data with a flaw, or with parameters a reader could take differently, is never coded again. ### JPEG images The JPEG pass re-encodes DCTDecode data without ever producing pixels, so the proof works at the same level. Both streams are read to their quantized coefficients, and must agree on width, height, component ids and order, chroma sampling, and the JFIF and Adobe color transform signalling. Every coefficient of every visible block must then be identical. Entropy coding, progressive or baseline layout, restart markers and the segments readers ignore (Exif, XMP, embedded ICC profiles, comments) may differ. Equal coefficients decode to equal pixels in any conforming decoder, which is why this counts as lossless. With `JpegQuality` set, the proof accepts one more change and checks it exactly: each output quantization step must be at least the source step, and each output coefficient must equal the source coefficient requantized with the integer rounding the optimizer uses. Any other change, including one coefficient off by one, fails verification. `pdfnetstack verify --jpeg-quality` and `PdfEquivalence.FindDifference(..., allowJpegRequantization: true)` apply the same rule. The lossy image options (`MaxImageDpi`, `ConvertImagesToJpeg`) change pixels, so their proof works on pixels and is a bound, not an identity. Only images the optimizer changed get it, and `PdfEquivalence` grants it only to images the optimizer could have changed: soft masks, masks, stencil and color keyed images must match exactly, and so must the size of images with a /Mask or a /Matte soft mask. An image whose data is unchanged must keep its size, and so must a bilevel image, a JPEG 2000 image that stays JPEG 2000 and a JBIG2 image that stays JBIG2: the optimizer changes those only by reducing them. Every other dictionary entry must still match, except that a JPEG 2000 image without /BitsPerComponent gains /BitsPerComponent 8 as samples or a JPEG; the output must have the same components and bit depth, no larger a size, the same aspect within a pixel, and decode as its dictionary says (raw samples, or one JPEG, JPEG 2000, JBIG2 or CCITT image). Both images are decoded and the source is resampled to the output's size with the same filter: Lanczos, or for bilevel images the reduction described below. Then each channel's mean may move by at most 4 steps, 32 pixel thumbnails may differ by at most 12 steps on average, which catches mirrored, shifted or swapped pictures, and luminance SSIM must reach `MinImageSsim`, or 0.5 when no gate is set. With a gate, requantized JPEGs must also reach it. The optimizer checks the same bounds on every JPEG it makes before choosing it, so it never writes an image the proof would reject. `pdfnetstack verify --min-ssim` and `PdfEquivalence.FindDifference(..., minImageSsim)` apply the same checks to any two files. A JPEG 2000 image counts only when its dictionary describes it fully: a gray or RGB /ColorSpace, no /Decode, no /SMaskInData, /BitsPerComponent 8 or none, and data with exactly that many color channels, no opacity channel and colors that are not sYCC. A bilevel image (raw, CCITT data every reader takes alike, or JBIG2) stays bilevel when reduced. Each new pixel takes the share of its area that the 1 bits cover in the source, an exact box filter. The less common value wins a pixel it covers more than half of, and also a pixel it covers at least a quarter of that is a ridge across or along the image (covered more than the neighbour before it and at least as much as the one after it): the middle of a stroke thinner than the new pixels, which plain thresholding would erase. The arithmetic is exact and in a fixed order, so the proof reproduces the reduction bit for bit and the optimizer's own output reaches SSIM 1 against it. The coefficient reader is itself checked against libjpeg: test fixtures written by libjpeg-turbo in baseline, optimized, progressive and restart marker layouts of the same image must decode to identical coefficients, and scaled tables must match libjpeg's own for the same quality. What it does not check: the trailer keys that describe file layout (`/Size`, `/Prev`, `/XRefStm`), object numbers and generations, and byte offsets. None of these affect rendering. The checker is public as `PdfEquivalence.FindDifference` and on the command line as `pdfnetstack verify`, so you can use it to validate any other tool's output. ## 2. Independent render comparison `tools/render_compare.py` renders both files with PDFium (through `pypdfium2`, a development tool only, not a dependency of any package) and compares page count, page geometry, extracted text per page and every RGB pixel of the first pages and the last page. ```bash pip install pypdfium2 numpy pdfnetstack batch ./corpus ./out python tools/render_compare.py ./corpus ./out --dpi 150 --pages 6 ``` Latest run: 1,068 optimized documents, 2,085 pages at 150 DPI, zero differing pixels. Structural passes, on synthetic samples from `tools/make_optimizer_samples.py` (unused resources, verbose content, separately embedded subsets of one font, inherited attributes with bookmarks, and all of them in one file): 88 pages rendered at 100 DPI from the default output and from the linearized output, zero differing pixels, identical text. Every linearized output passed qpdf 12.4's `--check-linearization`. ### Public benchmark corpus The documents above are private. `eng/corpus/manifest.json` lists a public corpus anyone can download and check: 77 public domain and openly licensed PDFs, each with its URL, SHA-256, size, license and what it exercises (scans with JBIG2, JPEG 2000 and CCITT images, Chinese, Japanese and Korean text, forms, XFA, tagged, signed, encrypted, linearized, damaged and PDF 2.0 files). `tools/corpus_bench.py` optimizes each one with a lossless and a lossy profile, proves every rewrite again with `PdfEquivalence`, compares the text and decoded images the library reads before and after, and renders the first pages and the last page of both with PDFium: ```bash python tools/fetch_corpus.py dotnet build tools/PdfNetStack.CorpusRunner -c Release python tools/corpus_bench.py --dpi 100 --pages 6 ``` First run: 65 of 77 documents rewritten (the other 12 are signed or encrypted), 217 pages at 100 DPI identical after the lossless rewrites, the same text and decoded images before and after, no rewrite failing its proof. Lossy rewrites lost at least 37.6 dB PSNR on any page but one: a 1.8 KiB ImageMagick test file of tiny images, at 30.4 dB. Run with bilevel, JBIG2 and JPEG 2000 images in the lossy profile: again no failure, lossless rewrites identical, lossy saving 29.5% (27.2% before). Pages of CCITT scans reduced to 150 dpi bilevel measure 22.8 to 22.9 dB PSNR against the source at 100 DPI, and a 1.8 KiB ImageMagick fax test at 17.2 dB: a bilevel page differs from its source along every stroke edge, so its PSNR says little. Read side by side, the reduced pages keep every stroke, rule and serif. ## 3. Tests - Syntax: literal and hex string escapes, names with `#xx`, number canonicalization, references, nesting limits, and a randomized round trip of strings and names through the serializer. - Filters: known vectors for ASCII85, ASCIIHex and RunLength, a reference LZW encoder for both EarlyChange modes, Flate with raw deflate fallback, corrupt data, a decompression bomb, PNG predictor round trips across bit depths, TIFF predictor. - Reader: classic and stream xrefs, object streams, incremental updates, `/Prev` loops, shifted offsets, missing `startxref`, wrong stream lengths, missing objects, garbage input. - Optimizer: every pass, every refusal rule, dedup identity rules, PDF/A-1 layout, stripping, cancellation, atomic file output. - Structural passes: pruning through forms, patterns, Type 3 fonts, appearances, inherited and shared resources, `/DR`, unreadable content and names split across parts; compaction of numbers, names, strings, comments and inline images, a randomized round trip, and every kind of token change caught; font merging with glyph conflicts, glyphs past the end of a subset and changed outlines; mutated inputs that must never throw. - Linearization: the layout checks above on documents with one and many pages, shared fonts, bookmarks in and out of the first page, form fields, an open action and inherited attributes; page trees it cannot lay out; mutated inputs. - JPEG: libjpeg-turbo fixtures (4:2:0, 4:2:2, 4:4:4, gray, Adobe CMYK, untransformed RGB, odd sizes) in baseline, optimized, progressive and restart marker layouts; lossless and requantized round trips; soft masks, Flate wrapped JPEG, color space mismatches, palettes, damaged and truncated data, unsupported coding processes, and mutated inputs that must never throw. - Bilevel images: the Group 4 encoder byte for byte against libtiff, round trips of random rows at widths from 1 to 9000 pixels with runs past 2560 in both polarities, the libtiff Group 3 fixtures coded again as Group 4, stencil masks, palettes, /Decode and predictors; Group 3 with flaws or parameters readers take differently left alone; a changed pixel, an inverted reading, short data and ambiguous parameters each caught by the proof; reductions of CCITT, JBIG2 and raw scans with thin strokes kept, and bad reductions (another page, too dark, plain subsampling, gray output) rejected; JPEG 2000 converted and reduced, and the JPEG 2000 images the dictionary cannot describe left alone; mutated fax and JBIG2 data that must never throw. - Fuzzing: mutated inputs (byte flips, deletions, truncation, injected tokens, duplicated regions) must never throw and must produce either the exact input or a verified rewrite. CI runs 600 cases; set `PDFNETSTACK_FUZZ_ITERATIONS` for a long soak. 20,000 cases pass locally. ## Reporting a fidelity bug If an optimized file looks different from its source in any viewer, that is the most serious class of bug in this project. Please open an issue with the viewer and version, and if you can share it, the source file. `pdfnetstack verify` output for the pair helps triage. Source: https://www.jadev-corp.com/pdfnetstack/concepts/fidelity/ --- # Memory and performance > Plan memory and CPU for PdfNetStack in .NET services and batch jobs, cap parallelism, choose compression effort, and publish with Native AOT. What PdfNetStack costs in memory and time, and the settings that change it. ## Short answer The optimizer reads a document whole into memory and processes it there. Plan for about 10 times the file size, more for image heavy files. From 1.1, `PdfDocument.OpenOnDemand` reads and edits a file of any size page by page, without loading it. `MaxDegreeOfParallelism` is the main lever on peak memory, and `Compression` is the main lever on time. ## Memory PDFs are random access by design and every optimizer pass touches most of the file, so the input is read into one buffer and stream data is sliced from it, never copied until it is re-encoded. Peak memory then depends on the decoded size of the streams being worked on and on how many are worked on at once. | Setting | Effect on memory | |---|---| | `MaxDegreeOfParallelism` (default: processor count) | Caps the worker threads of stream recompression and verification, so fewer streams are decoded at once. Lower it for image heavy files | | `MaxDecodedStreamBytes` (default 256 MiB) | Ceiling for any single decoded stream; larger streams are left untouched | | `MaxInputBytes` (default 1 GiB) | Larger inputs are returned unchanged. `OptimizeAsync` buffers at most this much and streams anything larger through to the output untouched | | `Streaming` (1.1, default false) | The source is read on demand and the rewrite goes through a temporary file, proven there: memory follows the number of objects and a batch of at most 32 MiB of source data, not the file | `PdfDocument.Open(ReadOnlyMemory)` references the buffer you pass instead of copying it, so the buffer must not change while the document is in use. The stream and file overloads read the input into memory first, once (1.1). Opened on demand (`PdfLoadMode.OnDemand`, 1.1), a document reads only the cross-reference data and the page tree, parses other objects when first used and reads stream data each time it is needed, so memory follows what your code touches; `SaveIncremental` then appends only the changes. `PdfLoadOptions` also caps decoded streams and image pixels per document. See [Large files](https://www.jadev-corp.com/guides/large-files/). A measured example from the README: a 631 page catalogue of 72.6 MiB, already Flate compressed, took about 5 seconds with all cores and peaked at a working set of about 690 MB. ## Time | Setting | Effect | |---|---| | `Compression = CompressionEffort.Maximum` (default) | zlib level 9 | | `Compression = CompressionEffort.Balanced` | zlib level 6. On the reference corpus it cut total time by about a third for about 2% larger output | | `Compression = CompressionEffort.Fast` | zlib level 1, for throughput bound batch jobs | | `Compression = CompressionEffort.Exhaustive` (1.1) | An optimal parsing Deflate search, kept only where it beats level 9. On synthetic files it saved 3 to 5% more on text, scans and photographs, for 2 to 150 times the CPU time | | `MaxImageDpi`, `ConvertImagesToJpeg`, `MinImageSsim` (1.1) | The lossy image options decode the images they consider to pixels, resample and encode them, and the proof decodes them again, so they cost time and memory the lossless passes do not. Without them the optimizer never decodes pixels | | `Verify = false` | Skips the equivalence proof. Only for inputs a validated pipeline already checked | | `ObjectsPerStream` (default 200) | Larger compresses better, smaller lets viewers load single pages faster | Parsing and graph work run on one thread. Stream recompression and stream comparison run in parallel, after every object they need has been resolved. On the reference corpus of 1,296 files (569 MiB), the whole run took about 65 seconds on an 8 core machine, 8 to 9 MB/s, lossless JPEG re-encoding included. See [Benchmarks](https://www.jadev-corp.com/benchmarks/). ## Many documents at once For a batch, run documents in parallel and give each one thread rather than giving one document every core. `pdfnetstack batch` does this by default: `--jobs` documents at once (default: all cores), each with one thread unless `--threads` is given. In a service, set `MaxDegreeOfParallelism` to bound the CPU and memory one request can take. ## Native AOT and trimming On .NET 8 and .NET 10 the `PdfNetStack`, `PdfNetStack.Html` and `PdfNetStack.Ocr` libraries are marked AOT compatible, which also enables the trim analyzers. CI publishes the command line tool and a smoke test app that roots all three libraries with Native AOT for linux-x64 and linux-arm64, fails on any trim or AOT warning, and runs the result. The core and HTML libraries have no native code to ship per platform. For serverless hosts, see [AWS Lambda](https://www.jadev-corp.com/guides/aws-lambda/) and [Azure Functions](https://www.jadev-corp.com/guides/azure-functions/). ## .NET Framework and .NET Standard 2.0 The `netstandard2.0` build runs on .NET Framework 4.6.2 to 4.8.1, Mono, Unity and .NET Core 2.0 and later, with Microsoft's `System.Memory` as its only dependency. .NET Standard's zlib has no level 9, so `CompressionEffort.Maximum` compresses a little less there. The full test suite and the real world corpus pass on .NET Framework 4.8. On .NET 9 and later, `System.IO.Compression` uses zlib-ng. ## Limits - In memory, the default, the optimizer needs the whole document, and so do `ToArray` and a full `Save` with the optimizer on. A save with `Optimize = false` is written progressively, and the optimizer's `Streaming` mode (1.1) keeps memory bounded for files larger than memory; see [Large files](https://www.jadev-corp.com/guides/large-files/). - Linearized output (`Linearize`, 1.1) writes every object on its own, without object streams, so it can be larger than the default output. - Faster text extraction and HTML layout are roadmap items. ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/): every optimizer option - [Safety model](https://www.jadev-corp.com/concepts/safety-model/): the limits that bound hostile input - [Architecture](https://www.jadev-corp.com/concepts/architecture/): the pipeline and its threading - API: [PdfOptimizerOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizeroptions/), [CompressionEffort](https://www.jadev-corp.com/api/pdfnetstack/compressioneffort/) ## How much memory does PdfNetStack need to optimize a PDF? The optimizer holds the whole document in memory. Plan for about 10 times the file size, more for image heavy files, or lower MaxDegreeOfParallelism. To read or edit a large file without loading it, open it with PdfDocument.OpenOnDemand (1.1). ## Is PdfNetStack compatible with Native AOT and trimming? Yes, on .NET 8 and later. The core, HTML and OCR libraries are marked AOT compatible, and CI publishes them with Native AOT on Linux x64 and arm64 and fails on any trim or AOT warning. Source: https://www.jadev-corp.com/pdfnetstack/concepts/memory-and-performance/ --- # Safety model > Which PDFs the PdfNetStack optimizer refuses to rewrite, how hostile input is bounded, and why the output is always the verified rewrite or your input. The optimizer has one promise: the file it writes is either a rewrite proven equivalent to your input, or your input unchanged. This page lists the rules behind that promise. ## Short answer Before rewriting, the optimizer checks the file. Encrypted, signed, incrementally updated, damaged and oversized files are returned unchanged with the disposition `UnsupportedRetained` and a reason. After rewriting, it parses its own output and compares it to the source; any difference returns the input with `OriginalRetained`. Every parser and decoder is bounded, so hostile input ends in a result, not a hang or a crash. ## Files returned unchanged | The file is | Reason reported | To rewrite it anyway | |---|---|---| | Not a readable PDF | `Not a readable PDF: ...` | Not possible | | Larger than `MaxInputBytes` (1 GiB) | `Input exceeds the configured size limit.` | Raise `MaxInputBytes` | | Encrypted | `Encrypted PDF.` | Open it with the owner password and save it, see [Encryption](https://www.jadev-corp.com/guides/encryption/) | | Digitally signed, or with usage rights: any `/ByteRange`, signature dictionary, `/Perms` or `/DSS` | `Digitally signed PDF.` or `Digitally signed or usage-rights enabled PDF.` | Never: a rewrite would invalidate the signatures | | Incrementally updated (more than one revision) | `PDF has N revisions (incremental updates). ...` | `AllowIncrementalUpdates`: the visible document is unchanged, earlier revisions are discarded | | Damaged: cross-reference reconstruction, repaired stream lengths, unreadable objects, or references whose generation numbers disagree with the cross-reference data | `PDF is damaged and needed repair. ...` and similar | `AllowRepairedFiles`: the output is a clean file, but another reader may have repaired the input differently | A rewrite is also discarded, with `OriginalRetained`, when it saves less than `MinSavingPercent` (1% by default), when verification finds a difference, or when the rewrite fails for any other reason. The reason says which. PDF/A-1 files are detected from their XMP metadata and written with a classic cross-reference table and no object streams, because PDF/A-1 predates both. ## Hostile input is bounded | Threat | Bound | |---|---| | Deep nesting of arrays and dictionaries | Nesting depth capped at 256 | | Chains of references | Resolution depth capped | | Decompression bombs | Each decoded stream is capped by `MaxDecodedStreamBytes` (256 MiB by default); streams over the limit are left untouched | | `/Prev` loops in the cross-reference chain | Detected and terminated | | Wrong stream lengths | Recovered from the `endstream` keyword | | Unreadable objects | Recorded, never thrown out of the optimizer | | JPEG data | Oversubscribed Huffman tables are refused, at most 64 scans per image, frames that claim more blocks than their data can code are refused | | Content streams | Operations, nesting of forms and graphics states, and decoded bytes are capped | | Images | Pixel counts are capped before any buffer is allocated: 2^28 by default, lower per document with `PdfLoadOptions.MaxImagePixels` (1.1) and per image file with `PdfImage.Load(data, page, maxPixels)` | | Fonts | Damaged font files raise `FontFormatException`; WOFF and WOFF2 output is capped at 128 MB | Opening a document for editing applies the same bounds. A file that cannot be read raises `PdfFormatException`, the one exception the parser documents for bad data, and decoders such as the image, font and SVG readers either raise their documented exception or stop at their limit. ## Verification The verifier walks the source and output graphs in lockstep from the trailer. Dictionaries must have the same keys and values, arrays the same items, references must line up, and every stream must decode to identical bytes with identical remaining filters. JPEG images are compared by DCT coefficient. Encoding keys (`/Length`, `/Filter`, `/DecodeParms`, `/DL`) are the only other differences it accepts. With `JpegQuality` set, a JPEG image may also differ by exactly the requantization the optimizer applied, and by nothing else. The structural passes of 1.1 change more than encoding, and none is taken on trust. The verifier runs its own analysis of the source: a resource entry may be missing only where no content can name it, a rewritten content stream must read as the same tokens, a merged font program must keep every glyph shown with the same outline and metrics, and streams may merge when they decode to the same bytes. Anything the analysis cannot read for certain keeps its resources, and when its work budget runs out nothing is pruned. The [fidelity](https://www.jadev-corp.com/concepts/fidelity/) page gives every rule. The lossy image options of 1.1 (`MaxImageDpi`, `ConvertImagesToJpeg`) change pixels, so their proof is a bound rather than an identity, and only for the images the optimizer changed. A changed image must keep its components and aspect, must not grow, and is decoded and compared with the source resampled to its new size: mean colors, a coarse thumbnail, and luminance SSIM of at least `MinImageSsim` (0.5 without a gate). Soft masks, masks, stencil and color keyed images must still match exactly, and every other part of the document as before. `Verify = false` skips this step. Turn it off only for inputs a validated pipeline has already checked. The full method is on the [fidelity](https://www.jadev-corp.com/concepts/fidelity/) page. ## Tests behind the rules The test suite covers every refusal rule, truncated and malicious input for every decoder, and fuzzing: mutated files (byte flips, deletions, truncation, injected tokens, duplicated regions) must never throw and must produce either the exact input or a verified rewrite. CI runs 600 fuzz cases on every build. ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/): the options named on this page - [Fidelity and verification](https://www.jadev-corp.com/concepts/fidelity/): what equivalent means, and how it is checked - [Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/): sizing the limits for your workload - API: [PdfOptimizerOptions](https://www.jadev-corp.com/api/pdfnetstack/pdfoptimizeroptions/), [OptimizationDisposition](https://www.jadev-corp.com/api/pdfnetstack/optimizationdisposition/), [PdfFormatException](https://www.jadev-corp.com/api/pdfnetstack/pdfformatexception/) ## Is PdfNetStack safe to run on PDFs from untrusted sources? It is built for it. Parsing and decoding are bounded in depth, size and work, damaged objects never crash a run, and the optimizer returns the input unchanged rather than throwing on a file it cannot read. ## Will the optimizer ever break a signed PDF? No. Any file with a signature byte range, a signature dictionary, usage rights or a document security store is returned byte for byte. Source: https://www.jadev-corp.com/pdfnetstack/concepts/safety-model/ --- # Frequently asked questions > Answers about PdfNetStack licensing, platforms, native dependencies, lossless compression, HTML to PDF, Arabic text, PDF/A, OCR and memory use. Short answers to the questions people ask before they adopt PdfNetStack. Each one links to the page with the details below. ## Where to read more - Licensing and dependencies: [About](https://www.jadev-corp.com/about/) - Platforms and installation: [Getting started](https://www.jadev-corp.com/getting-started/) - Compression results and method: [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/), [Benchmarks](https://www.jadev-corp.com/benchmarks/), [Fidelity and verification](https://www.jadev-corp.com/concepts/fidelity/) - Encrypted, signed and damaged files: [Safety model](https://www.jadev-corp.com/concepts/safety-model/) - HTML to PDF: [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) and [CSS support](https://www.jadev-corp.com/guides/html-to-pdf/css-support/) - Arabic and right to left text: [Fonts and text](https://www.jadev-corp.com/guides/fonts-and-text/) - PDF/A: [PDF/A](https://www.jadev-corp.com/guides/pdf-a/) - OCR: [OCR](https://www.jadev-corp.com/guides/ocr/) - Memory: [Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/) - What comes next: [Roadmap](https://www.jadev-corp.com/roadmap/) A question that is not answered here? Ask it in the [issues](https://github.com/jams4code/pdf.netstack/issues) of the repository. ## Is PdfNetStack free for commercial use? Yes. PdfNetStack is MIT licensed. On .NET 8 and later the core package has no dependencies; on .NET Standard 2.0 it uses Microsoft's System.Memory, which is also MIT. CI blocks any runtime package that is not on a reviewed license allowlist. ## Does it need Ghostscript, a browser or other native binaries? No. The library, the HTML to PDF package and the command line tool are managed code and start no external process. The one exception is OCR. The optional OCR add-on and its pdfnetstack-ocr tool run their models on ONNX Runtime, and pdfnetstack ocr starts that separate tool. ## Which platforms and .NET versions does it run on? .NET 8, 9, 10 and later through the net8.0 and net10.0 targets, and .NET Framework 4.6.2 to 4.8.1, Mono, Unity and .NET Core 2.0+ through netstandard2.0. CI runs the tests on Windows, Linux and macOS, on .NET Framework 4.8, .NET 8 and .NET 10. ## Is it compatible with trimming and Native AOT? Yes. The net8.0 and net10.0 builds of the library are marked trim and AOT compatible, and CI publishes the command line tool with Native AOT, failing on any trim or AOT warning. ## Is the compression lossless? Yes, by default. Text, vectors, fonts, images, annotations, forms, bookmarks, structure tags, metadata and attachments are carried over, and every output is compared to its source before it is returned. Lossy changes happen only when you ask for them, as JPEG requantization with JpegQuality, and from 1.1 image downsampling with MaxImageDpi and conversion of raw photographs to JPEG, both checked against the source and optionally held back by a perceptual quality gate. ## How much smaller do files get? On a mixed corpus of 1,296 real world files, the 1,003 files that were optimized went from 568.9 MiB to 469.0 MiB, 17.6% saved, with zero differing pixels at 150 DPI. Image heavy files often gain 20 to 40%. Files from producers that already use object streams gain little. ## What happens with encrypted or signed PDFs? The optimizer returns them byte for byte, because rewriting would break the encryption or the signatures. The editing API opens encrypted files with a password and keeps their protection on save. Saving a signed document in full invalidates its signatures; from version 1.1, SaveIncremental appends the changes after the signed bytes and keeps them valid. ## Can PdfNetStack render PDF pages to images? No. Rendering pages to images is out of scope for the project. It extracts the images a page contains, and the text, but it does not rasterize pages. ## Is the HTML to PDF converter a headless browser? No. PdfNetStack.Html is a layout engine written in C# with the CSS cascade, block, inline, table, flex and grid layout and paged media. It does not run JavaScript, so pages must be rendered HTML. It ships as the PdfNetStack.Html package on NuGet. ## Does it handle Arabic, Indic and right to left text? Yes, from version 1.1. Text goes through the Unicode bidirectional algorithm, Unicode normalization and an OpenType shaper, so Arabic letters join and take their contextual forms, the nine major Indic scripts reorder and form their conjuncts, and right to left text copies and searches in logical order. Thai, Khmer, Myanmar and Hangul shaping are on the roadmap. ## Can it create PDF/A files? Yes, from version 1.1. Saving with PdfSaveOptions.Conformance writes PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e or 4f, after checking what cannot be repaired and listing every problem at once. PdfInvoice adds Factur-X and ZUGFeRD invoice data to PDF/A-3 files, and CI validates the output with veraPDF. ## Does it support long term signatures? Yes, from version 1.1. PdfNetStack signs to PAdES B-B, B-T, B-LT and B-LTA, and validates signatures against the trust anchors you supply. It makes no network calls; your application sends the timestamp requests and supplies the revocation data. ## Can it make scanned PDFs searchable? Yes, with the PdfNetStack.Ocr add-on, for Latin and Arabic script. It is a separate package on NuGet, with its models in PdfNetStack.Ocr.Models. The core library also has DrawInvisibleText to lay the words of any OCR engine over a scan. ## How much memory does it use? The optimizer holds the whole document in memory. Plan for about 10 times the file size, more for image heavy files, or lower MaxDegreeOfParallelism to cap memory. From version 1.1, PdfDocument.OpenOnDemand reads and edits a file of any size page by page without loading it, and SaveIncremental appends only the changes. ## Can it write linearized PDFs for fast web view? Yes, from version 1.1, with Linearize in PdfOptimizerOptions or pdfnetstack optimize --linearize. The output follows ISO 32000-1 Annex F with hint tables and is verified like any other rewrite. ## Is it safe to run on files from untrusted sources? That is a design goal. Nesting depth, decoded stream sizes, image sizes and loops in the file structure are bounded, and damaged input raises PdfFormatException instead of crashing or hanging. The HTML converter fetches nothing from the network unless you give it a resource loader. ## Which version should I use today? Version 1.2.0, on NuGet. It has every feature on this site, including the optimizer, editing, drawing, fonts, forms, signatures, PDF/A, extraction, HTML to PDF, OCR and the command line tools. ## Is PdfNetStack related to the PDFNet SDK? No. PDFNet is a commercial SDK from Apryse. PdfNetStack is an independent open source project by JADEV with no connection to it. ## Who maintains PdfNetStack? Jamal Abdelkhalek at JADEV created it and maintains it. Contributions are welcome under the contribution guide in the repository. Source: https://www.jadev-corp.com/pdfnetstack/faq/ --- # PDF compression benchmarks > PdfNetStack compression measured on a public corpus at every release and on 1,296 real world PDFs, with the method and how to reproduce it. What the optimizer saves on a public corpus that CI measures at every release, what it saved on a larger private corpus, and how to run the same measurement yourself. ## Short answer On the public corpus of 77 openly licensed PDFs, release 1.1.0 saved 12.1% losslessly, 23.4% with the balanced preset and 29.5% with the lossy profile, with zero failures. On a mixed private corpus of 1,296 files (569 MiB) with default options, PdfNetStack optimized 1,003 files from 568.9 MiB to 469.0 MiB, 17.6% saved, with zero verification failures. Every optimized file rendered identically to its original with PDFium at 150 DPI. The run took about 65 seconds on an 8 core machine. ## Public corpus CI downloads the documents listed in [eng/corpus/manifest.json](https://github.com/jams4code/pdf.netstack/blob/main/eng/corpus/manifest.json): public domain and openly licensed PDFs such as NIST and NASA publications, arXiv papers, government gazettes and test files from other PDF projects. They are never committed. The run happens every week, on demand, and for every version tag, when the report is attached to the GitHub release. Each file runs in its own process under three profiles: - **Lossless.** The default optimizer. - **Balanced.** The preset of that name: palettes for drawings and screenshots, JPEG quality 75 at full resolution, behind an SSIM gate of 0.95. - **Lossy.** JPEG quality 75, downsampling to 150 dpi, photographs to JPEG and progressive JPEG, behind the same gate. Every kept rewrite is checked again with `PdfEquivalence`, the text of every page and every image are compared with the original, and PDFium renders the first pages and the last page of both. A lossless rewrite must render identically. Release 1.1.0, 5 November 2026, on a 4 core Linux runner: | Profile | Documents optimized | Input | Output | Saved | Optimizer time | |---|---:|---:|---:|---:|---:| | Lossless | 65 of 77 | 196.6 MiB | 172.9 MiB | 12.1% | 44.4 s | | Balanced | 65 of 77 | 196.6 MiB | 150.6 MiB | 23.4% | 154.5 s | | Lossy | 65 of 77 | 196.6 MiB | 138.6 MiB | 29.5% | 497.4 s | Zero failures across the 77 documents. The 12 documents kept as they are were signed or encrypted files, which the optimizer never rewrites. The report with every document, its time, memory and render result is attached to the [1.1.0 release](https://github.com/jams4code/pdf.netstack/releases/tag/v1.1.0) as `corpus-report.md` and `corpus-report.json`. To run it yourself: ```bash pip install pypdfium2 numpy python tools/fetch_corpus.py --cache artifacts/corpus/files dotnet build tools/PdfNetStack.CorpusRunner -c Release python tools/corpus_bench.py --cache artifacts/corpus/files --out artifacts/corpus/report ``` ## Private corpus - **Corpus.** 1,296 files, 569 MiB: invoices, reports, manuals, scanned documents, presentations and a 631 page spare parts catalogue. The corpus is not public. - **Machine.** An 8 core Windows machine. - **Options.** Defaults: lossless, `CompressionEffort.Maximum`, verification on, 1% minimum saving. A second run added `AllowIncrementalUpdates` and `AllowRepairedFiles`. - **Lossless** means a conforming reader cannot tell the output from the input: same pages, content, fonts, images, annotations, forms, bookmarks, structure, metadata and attachments. - **Verified** means the optimizer parsed its own output and proved it equivalent to the source, object by object, with every stream compared after decoding and JPEG images compared by DCT coefficient. A file that fails is returned unchanged and counted as a verification failure. See [Fidelity and verification](https://www.jadev-corp.com/concepts/fidelity/). - **Render check.** Independent of the verifier: both files rendered with PDFium at 150 DPI, compared pixel for pixel, with page count, geometry and extracted text per page. The figures are those published with version 1.0.0, in October 2026, and cover the lossless passes only. The lossy profiles are measured on the public corpus above. ### Results | | | |---|---| | Optimized | 1,003 files, total size 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 (empty or resource fork files) | | Verification failures | 0 | | Throughput | about 65 s for the whole corpus, 8 to 9 MB/s, lossless JPEG re-encoding included | | With `AllowIncrementalUpdates` and `AllowRepairedFiles` | 1,118 files optimized, 20.1% saved | | Render check | 1,118 documents, 2,161 pages, zero differing pixels, identical extracted text | The 631 page catalogue, 72.6 MiB of CAD drawings already Flate compressed: 6,075 duplicate objects merged, 3.1% saved, about 5 seconds with all cores, peak working set about 690 MB. Image heavy files gain the most, often 20 to 40%, mostly from PNG prediction on raw images. Files from producers that already use object streams have little left to gain losslessly. Editing was checked the same way: every document in the corpus was rebuilt by importing all of its pages into a new document and saving it. 1,152 documents, 2,233 pages, zero differing pixels against the originals at 150 DPI. ## Measure your own files Measure without writing anything, then write the optimized files and a CSV report: ```bash dotnet tool install -g PdfNetStack.Tool pdfnetstack batch ./corpus # measure only pdfnetstack batch ./corpus ./out --report report.csv # write and report ``` The summary prints documents optimized, retained and unsupported, the sizes, the time, the throughput and the peak memory. The CSV has one row per document. See [Command line](https://www.jadev-corp.com/cli/#batch). Then render both sets with PDFium and compare them. The script is [tools/render_compare.py](https://github.com/jams4code/pdf.netstack/blob/main/tools/render_compare.py) in the repository; PDFium is a development tool here, not a dependency of any package. ```bash pip install pypdfium2 numpy python tools/render_compare.py ./corpus ./out --dpi 150 --pages 6 ``` ## What comes next Each release report is compared with the previous one, so a change in size, time or memory shows next to the release. This page shows no numbers for other tools. ## Related - [Compress PDF](https://www.jadev-corp.com/guides/compress-pdf/) - [Fidelity and verification](https://www.jadev-corp.com/concepts/fidelity/) - [Memory and performance](https://www.jadev-corp.com/concepts/memory-and-performance/) Source: https://www.jadev-corp.com/pdfnetstack/benchmarks/ --- # About PdfNetStack > Who builds PdfNetStack, why it exists, how it is licensed and how to contribute. An original MIT licensed PDF library for .NET by JADEV. PdfNetStack is a PDF library and command line tool for .NET, written in C# and published under the MIT license. ## Why it exists PdfNetStack gives .NET developers a PDF toolkit that is permissively licensed all the way down, safe to run on files from strangers, and honest about what it changes. It optimizes, creates, edits, merges, splits and protects PDFs, extracts their text and images, and converts HTML and CSS to PDF. It started with one narrow job: making PDFs smaller without shelling out to Ghostscript, without a copyleft or commercial dependency, and without silently losing forms, tags or metadata. That job set the rules the rest of the library follows: lossless by default, every rewrite verified, bounded on hostile input, and no native code in the core packages. ## Who builds it PdfNetStack is created and maintained by [Jamal Abdelkhalek](https://www.jadev-corp.com/jamal) at [JADEV](https://www.jadev-corp.com/en). The packages on NuGet are published by the `jadev` account. - Source and issues: [github.com/jams4code/pdf.netstack](https://github.com/jams4code/pdf.netstack) - Packages: [PdfNetStack on NuGet](https://www.nuget.org/packages/PdfNetStack) - Company: [jadev-corp.com](https://www.jadev-corp.com/en) The repository is named `pdf.netstack`. The product, the packages and the namespaces are all called PdfNetStack. ## Original work PdfNetStack is a greenfield project. Its code is original and written from the published specifications: ISO 32000 for PDF, OpenType, Unicode, and the HTML and CSS standards. Over years of building software, its author has used many PDF and document tools, and their strengths shaped the feature set and the API: PdfSharp and MigraDoc, PDFTron (now Apryse), iText, QuestPDF, wkhtmltopdf, WeasyPrint, PDFium, pdf.js, Poppler and qpdf. HarfBuzz, libjpeg, libtiff and PDFium also serve as reference implementations in the test suite, where outputs are compared. No source code from any of these projects is included. The few third party data tables the library uses, such as the Unicode character data and the Adobe Glyph List, are listed with their licenses in [THIRD-PARTY-NOTICES.md](https://github.com/jams4code/pdf.netstack/blob/main/THIRD-PARTY-NOTICES.md). Product names are trademarks of their respective owners and are mentioned for reference only, without affiliation or endorsement. PdfNetStack is not related to the PDFNet SDK of Apryse. ## License MIT. See [LICENSE](https://github.com/jams4code/pdf.netstack/blob/main/LICENSE). On .NET 8 and later the core package has no dependencies; on .NET Standard 2.0 it uses Microsoft's `System.Memory`, also MIT. CI blocks any runtime package that is not on a reviewed license allowlist. Runtime dependency licenses are listed in THIRD-PARTY-NOTICES.md. ## Contributing Bug reports with a file that is handled wrongly are the most useful contribution. Issues labelled `good first issue` and `help wanted` are open to anyone, and roadmap items start with an issue to agree on the approach. The rules are in [CONTRIBUTING.md](https://github.com/jams4code/pdf.netstack/blob/main/CONTRIBUTING.md), including the policy for AI assisted contributions. Security issues follow the [security policy](https://github.com/jams4code/pdf.netstack/blob/main/SECURITY.md). ## This site This site is built from the `website` folder of the repository, in the same pull requests as the code it describes. The API reference is generated from the XML documentation of the source, and every C# sample is compiled and run in CI. Each page is also available as Markdown at the same address with `.md` instead of the trailing slash, and the whole documentation is in [llms.txt](https://www.jadev-corp.com/llms.txt) for tools and assistants. Source: https://www.jadev-corp.com/pdfnetstack/about/ --- # Compare PdfNetStack with other PDF tools > How PdfNetStack compares with PDFsharp, QuestPDF, iText, PdfPig, wkhtmltopdf, headless Chromium, Ghostscript and qpdf. Licenses and features, with sources. PdfNetStack is one of several ways to work with PDF files from .NET. These pages set it next to the libraries and tools developers usually weigh it against, with a source for every fact about the other products. ## How to read these tables - Facts about PdfNetStack come from its repository. **1.0**, **1.1** and **1.2** say which release, all on NuGet, added a feature. - Facts about other products come from their own websites, repositories, documentation and NuGet packages, linked on each product's page. - **Not listed** means the feature is not in the product's own documentation or feature list on the check date. It does not mean the feature is impossible. - Versions are the latest stable releases on NuGet or the product's site on the check date. ## Libraries | | PdfNetStack | [PDFsharp and MigraDoc](https://www.jadev-corp.com/compare/pdfsharp/) | [QuestPDF](https://www.jadev-corp.com/compare/questpdf/) | [iText](https://www.jadev-corp.com/compare/itext/) | [PdfPig](https://www.jadev-corp.com/compare/pdfpig/) | |---|---|---|---|---|---| | License | MIT | AGPL-3.0 or commercial (6.5.0); MIT up to 6.2.4 | Community License (free under conditions) or paid | AGPL-3.0 or commercial | Apache-2.0 | | Native binaries in the package | None | None | Yes: Skia based layer and qpdf, per platform | None | None | | Create PDFs | 1.1: canvas, fonts, paragraphs | Yes | Yes, fluent layout API | Yes | Basic, ASCII text only | | Edit pages, merge, split | 1.0 | Yes | Merge, extract pages, overlays | Yes | Merge, very limited editing | | Compress existing PDFs | 1.0, lossless, every output verified; 1.1 adds opt-in image downsampling and JPEG conversion | Not listed | Not listed | pdfOptimizer add-on, with a lossless profile | Not listed | | HTML to PDF | 1.1, `PdfNetStack.Html` | Not out of the box, per its FAQ | No, documents are built in code | pdfHTML add-on | Not listed | | Forms | 1.1: create, fill, flatten | Limited AcroForm support, per its FAQ | Not listed | Yes | Read only | | Digital signatures | 1.1: PAdES B-B to B-LTA, validation against your trust anchors | Yes, since 6.2.0 | Not listed | Yes | Not listed | | PDF/A | 1.1: 1b, 2b, 2u, 3b, 3u, 4, 4e, 4f; Factur-X and ZUGFeRD | Yes, for new documents | 2b, 2u, 2a, 3b, 3u, 3a | Yes | 1a, 1b, 2a, 2b, 3a, 3b, for documents it creates | | Encryption | 1.0: AES-256, AES-128 | Yes | Yes | Yes | Opens protected files | | Text extraction | 1.1: text, words, letters | Low level only, per its FAQ | Not listed | Yes | Yes, with layout analysis | | OCR | 1.1: add-on package; 1.2 adds a Tesseract engine | Not listed | Not listed | pdfOCR add-on | Not listed | | Render PDF pages to images | No, out of scope | Not listed | Not listed | Not listed | Not listed | ## Tools and browsers | | PdfNetStack | [wkhtmltopdf](https://www.jadev-corp.com/compare/wkhtmltopdf/) | [Puppeteer Sharp](https://www.jadev-corp.com/compare/headless-chromium/) | [Playwright for .NET](https://www.jadev-corp.com/compare/headless-chromium/) | [Ghostscript](https://www.jadev-corp.com/compare/ghostscript-qpdf/) | [qpdf](https://www.jadev-corp.com/compare/ghostscript-qpdf/) | |---|---|---|---|---|---|---| | License | MIT | LGPL-3.0 | MIT | MIT | AGPL-3.0 or commercial | Apache-2.0 | | What it runs | Managed .NET code | A native program or C library on Qt WebKit | A downloaded browser; PDF output needs Chrome headless | A bundled Node.js and downloaded browsers | A native interpreter | A native C++ library and command line tool | | HTML to PDF | 1.1, no JavaScript | Yes | Yes | Yes | No, reads PostScript and PDF | No, transforms existing PDFs | | Compress or restructure PDFs | 1.0, lossless, verified; 1.1 adds opt-in lossy image options | Not listed | Not listed | Not listed | Yes, by re-creating the PDF | Yes, content preserving | | Render PDF pages to images | No | Not listed | Not listed | Not listed | Yes | No | The wkhtmltopdf repository was archived by its owner on 2023-01-02. ## Pages - [PDFsharp and MigraDoc](https://www.jadev-corp.com/compare/pdfsharp/): creating documents in code - [QuestPDF](https://www.jadev-corp.com/compare/questpdf/): documents from a fluent layout API - [iText](https://www.jadev-corp.com/compare/itext/): a broad PDF SDK under AGPL or a commercial license - [PdfPig](https://www.jadev-corp.com/compare/pdfpig/): reading text and content - [wkhtmltopdf](https://www.jadev-corp.com/compare/wkhtmltopdf/): HTML to PDF on Qt WebKit - [Puppeteer Sharp and Playwright](https://www.jadev-corp.com/compare/headless-chromium/): HTML to PDF in a real browser - [Ghostscript and qpdf](https://www.jadev-corp.com/compare/ghostscript-qpdf/): compression and structure Each page ends with a section on when to pick the other tool. Commercial SDKs are not covered here. Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/ --- # PdfNetStack, Ghostscript and qpdf compared > Ghostscript and qpdf next to PdfNetStack for compressing and restructuring PDFs: how each works, licenses in plain words, and when to pick each. Ghostscript and qpdf are native tools that can make PDFs smaller or change their structure, run from a .NET application as external processes or native libraries. PdfNetStack does that work in managed .NET code. This page sets the three side by side. ## What each does well **Ghostscript.** "Ghostscript is an interpreter for the PostScript® language and PDF files" ([site](https://www.ghostscript.com/)). The site lists compression, PDF/A conversion and ZUGFeRD and Factur-X e-invoices among its uses. Its pdfwrite device re-creates a document: "a new PDF file is being created, which should look the same as the original". Presets such as `/screen` and `/ebook` downsample images to 72 and 150 dpi, and the documentation warns that they "may result in a PDF of poorer quality" than the input. pdfwrite writes object streams by default, and can write linearized and encrypted files, and PDF/A versions 1 to 3 at conformance level b ([pdfwrite documentation](https://ghostscript.readthedocs.io/en/latest/VectorDevices.html)). Its raster devices render pages to PNG, JPEG and TIFF ([devices](https://ghostscript.readthedocs.io/en/latest/Devices.html)). **qpdf.** qpdf "is a command-line tool and C++ library that performs content-preserving transformations on PDF files. It supports linearization, encryption, and numerous other features." It also splits and merges files, and it "does not render PDFs or perform text extraction" ([repository](https://github.com/qpdf/qpdf)). Its manual covers generating object streams, recompressing Flate streams at a chosen level, linearized output, splitting pages into files, and an opt-in `--optimize-images` that recompresses images as JPEG when that makes them smaller ([command line documentation](https://qpdf.readthedocs.io/en/stable/cli.html)). **PdfNetStack.** A lossless optimizer in managed .NET code: object streams, a compressed cross-reference stream, removal of unreachable objects and unused resources, Flate recompression, PNG prediction for raw images, lossless JPEG re-encoding and deduplication. By default it then reads its own output back, compares it to the source object by object, and returns the original bytes when anything differs ([compression](https://www.jadev-corp.com/guides/compress-pdf/), [fidelity](https://www.jadev-corp.com/concepts/fidelity/)). Lossy options are opt-in: JPEG requantization since 1.0, and since 1.1 downsampling, conversion of photographs to JPEG, palettes for drawings and screenshots, and the `Balanced` and `Smallest` presets. It also edits, merges, splits and encrypts (1.0). ## Two approaches to smaller files Ghostscript rebuilds the document from what it interprets. That allows changes such as image downsampling or a new color space, and the result is a new document that should look the same as the original. Its documentation says that when a modified file is the goal, "pdfwrite performs these tasks well". By default, qpdf and PdfNetStack keep the document's content and change how it is stored. qpdf's `--optimize-images` is opt-in: it recompresses images as JPEG, at a quality set by `--jpeg-quality`. PdfNetStack is lossless by default and, from 1.1, can downsample images, convert photographs to JPEG and reduce drawings to a palette when you opt in, with an optional perceptual quality gate (`MinImageSsim`). It adds a proof, on by default: every rewrite is compared to its source, lossy image changes are checked against the source image, and signed, encrypted and damaged files are returned unchanged ([safety model](https://www.jadev-corp.com/concepts/safety-model/)). ## Licensing in plain words - **Ghostscript.** The GNU AGPL, version 3 or later, in the repository's license file ([LICENSE](https://github.com/ArtifexSoftware/ghostpdl/blob/master/LICENSE)), or a commercial license from Artifex. Its documentation says GhostPDL, which brings the Ghostscript family together, "is available both under the GNU Affero GPL license and for commercial licensing from Artifex" ([documentation](https://ghostscript.readthedocs.io/en/latest/Readme.html)). Artifex's licensing page states: "You cannot deploy our open-source as part of a server-based application or service, without disclosing your own application’s full source code under AGPL to any users interacting with it." It adds: "If you can't meet the requirements of the GNU AGPLv3 above, a commercial license is required" ([Artifex licensing](https://artifex.com/licensing)). - **qpdf.** Apache License 2.0 ([repository](https://github.com/qpdf/qpdf)). - **PdfNetStack.** MIT. Read each license yourself, or ask your counsel, before you decide. This page is not legal advice. ## Features | | PdfNetStack | Ghostscript | qpdf | |---|---|---|---| | License | MIT | AGPL-3.0 or commercial | Apache-2.0 | | How it runs | Managed .NET library, in process | Native interpreter | Native C++ library and command line tool | | How it compresses | Rewrites storage, lossless by default, output verified | Re-creates the document | Rewrites storage, content preserved | | Object streams | Yes | Yes, on by default (`WriteObjStms`) | Yes, generated on request | | Flate recompression | Yes, level 9 by default | Yes, level set by `StreamEffort` | Yes, chosen level | | Image downsampling | 1.1, opt-in (`MaxImageDpi`) | Yes, with presets or per image type | Not listed | | JPEG | Lossless re-encoding; opt-in requantization; 1.1: opt-in conversion of photographs | Encodes images as JPEG; passes JPEG data through by default (`PassThroughJPEGImages`) | Opt-in: images to JPEG (`--optimize-images`) | | Equivalence check of the output | Yes, on by default | Not listed | Not listed | | Linearized output | 1.1, opt-in (`Linearize`) | Yes (`-dFastWebView`) | Yes | | Encryption | AES-256 and AES-128 | Yes, revision 2 or 3 | Yes | | Split and merge | Yes | Page selection (`-sPageList`), in a re-created file | Yes | | PDF/A output | 1.1: PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e, 4f | PDF/A-1, 2 and 3, level b | No, per its manual | | Render pages to images | No | Yes | No | Sources: [Ghostscript site](https://www.ghostscript.com/), [pdfwrite](https://ghostscript.readthedocs.io/en/latest/VectorDevices.html), [Ghostscript devices](https://ghostscript.readthedocs.io/en/latest/Devices.html), [Ghostscript usage](https://ghostscript.readthedocs.io/en/latest/Use.html) (page selection), [qpdf repository](https://github.com/qpdf/qpdf), [qpdf command line](https://qpdf.readthedocs.io/en/stable/cli.html). "Not listed" means the feature is not in the pages cited on the check date. ## When to pick Ghostscript - You want presets such as `/screen` and `/ebook` that re-create the whole document for size, and accept the changes that brings. - You need to render pages to images, or to process PostScript. - The AGPL fits your deployment, or a commercial license from Artifex suits you. ## When to pick qpdf - You want to check existing linearized files (`--check-linearization`) or inspect a file's structure from the command line. - You want a structural tool from a shell script or a C++ program, outside .NET. - You want Apache-2.0 licensing. ## When to pick PdfNetStack - You want smaller files with nothing lost, and a proof of it for every file. - You want to stay in managed .NET code, with no external process or native binary to ship. - You want MIT licensing. ## Related - [Compare overview](https://www.jadev-corp.com/compare/) - [Compress PDF files](https://www.jadev-corp.com/guides/compress-pdf/) - [Command line](https://www.jadev-corp.com/cli/) - [Benchmarks](https://www.jadev-corp.com/benchmarks/) Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/ghostscript-qpdf/ --- # PdfNetStack and headless Chromium compared > Puppeteer Sharp and Playwright print HTML to PDF through a browser. How that compares with PdfNetStack.Html, a C# layout engine, and when to use each. Puppeteer Sharp and Playwright for .NET drive a real browser from C#, and a browser can print a page to PDF. `PdfNetStack.Html` converts HTML and CSS to PDF with a layout engine written in C#, without a browser. It shipped in PdfNetStack 1.1 and is on NuGet. ## What each does well **Puppeteer Sharp.** A ".NET port of the official Node.JS Puppeteer API". It drives Chrome, and Firefox through WebDriver BiDi. Its examples download a browser with `BrowserFetcher`, open a page and call `page.PdfAsync` ([repository](https://github.com/hardkoded/puppeteer-sharp)). Its API documentation notes: "Generating a pdf is currently only supported in Chrome headless" ([API](https://www.puppeteersharp.com/api/PuppeteerSharp/Page/)). `PdfOptions` takes header and footer templates with page numbers, and options for a tagged PDF and a document outline ([PdfOptions](https://www.puppeteersharp.com/api/PuppeteerSharp/PdfOptions/)). **Playwright for .NET.** A browser automation library from Microsoft. Its NuGet package bundles Node.js runtimes for Windows, Linux and macOS ([NuGet](https://www.nuget.org/packages/Microsoft.Playwright/1.64.1), package contents), and `playwright.ps1 install` downloads Chromium, Firefox and WebKit builds ([browsers](https://playwright.dev/dotnet/docs/browsers)). `Page.PdfAsync` "generates a pdf of the page with `print` css media". Its options include header and footer templates with page numbers, a tagged PDF and a document outline ([API](https://playwright.dev/dotnet/docs/api/class-page#page-pdf)). Both run the page in a browser engine, so JavaScript runs and layout matches that browser. **PdfNetStack.Html.** A layout engine in C# that runs inside your process: the CSS cascade, block, inline, table, flex and grid layout, floats and positioning, paged media with `@page`, margin boxes, named pages and running headers and footers, repeated table headers, tagged PDF, web fonts, and right to left and Arabic text. Since 1.2, `hyphens: auto` hyphenates with the patterns of the `PdfNetStack.Hyphenation` package. No browser to download or keep updated, and nothing is fetched from the network unless you pass a resource loader ([HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/)). The result is a `PdfDocument` that can be signed, encrypted or saved as PDF/A with the rest of PdfNetStack. ## Licensing in plain words - **Puppeteer Sharp.** MIT ([NuGet](https://www.nuget.org/packages/PuppeteerSharp/25.15.0)). - **Playwright for .NET.** MIT ([NuGet](https://www.nuget.org/packages/Microsoft.Playwright/1.64.1)). The browsers they download come with their own licenses. - **PdfNetStack.Html.** MIT. Read each license yourself before you decide. This page is not legal advice. ## Features | | PdfNetStack.Html 1.2.0 | Puppeteer Sharp 25.15.0 | Playwright for .NET 1.64.1 | |---|---|---|---| | License | MIT | MIT | MIT | | What runs | Managed .NET code in your process | A downloaded browser; PDF output needs Chrome headless | A bundled Node.js runtime and downloaded browsers | | JavaScript | No | Yes | Yes | | Rendering | C# layout engine | The browser's engine | The browser's engine | | Network access during conversion | None without a resource loader | As the browser loads the page; requests can be intercepted and aborted | As the browser loads the page; requests can be routed and aborted | | Headers and footers with page numbers | CSS margin boxes and running elements | `HeaderTemplate` and `FooterTemplate` options | `HeaderTemplate` and `FooterTemplate` options | | CSS margin boxes, named pages, running elements | Yes | As the browser supports them | As the browser supports them | | Bookmarks from headings | Yes, on by default | `Outline` option, off by default | `Outline` option, off by default | | Tagged PDF | Yes, on by default | `Tagged` option, on by default | `Tagged` option, off by default | | Sign, encrypt, PDF/A on the result | Yes, with PdfNetStack | Not listed | Not listed | Sources: [Puppeteer Sharp repository](https://github.com/hardkoded/puppeteer-sharp), [Puppeteer Sharp `Page` API](https://www.puppeteersharp.com/api/PuppeteerSharp/Page/), [Puppeteer Sharp `PdfOptions` API](https://www.puppeteersharp.com/api/PuppeteerSharp/PdfOptions/) and [source at 25.15.0](https://github.com/hardkoded/puppeteer-sharp/blob/v25.15.0/lib/PuppeteerSharp/PdfOptions.cs) for the defaults, [Playwright browsers](https://playwright.dev/dotnet/docs/browsers), [Playwright `Page` API](https://playwright.dev/dotnet/docs/api/class-page#page-pdf), the NuGet pages linked above. Chrome added page margin boxes for printing and PDF export in version 131 ([New in Chrome 131](https://developer.chrome.com/blog/new-in-chrome-131)). "Not listed" means the feature is not in the product's own documentation on the check date. ## When to pick a headless browser - The page needs JavaScript: charts drawn by script, single page applications, content loaded after the page opens. `PdfNetStack.Html` does not run scripts. - You need output that matches what a specific browser shows on screen, including CSS features `PdfNetStack.Html` does not support yet, such as multi-column layout. - You already run browsers for testing and want PDF output from the same setup. ## When to pick PdfNetStack.Html - You render your own templates, invoices and reports, and want no browser binaries to download, patch and keep in your containers. - You convert HTML you do not fully trust and want no network access by default. - You want to sign, encrypt or save the result as PDF/A in the same library, or change it as a `PdfDocument` before saving. ## Related - [Compare overview](https://www.jadev-corp.com/compare/) - [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) - [CSS support](https://www.jadev-corp.com/guides/html-to-pdf/css-support/) Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/headless-chromium/ --- # PdfNetStack and iText compared > iText Core next to PdfNetStack for PDF work in .NET: strengths, the AGPL and commercial licenses in plain words, features, and when to pick iText. iText Core is a broad PDF library for .NET and Java, with add-ons for HTML, OCR, redaction, complex scripts and file size optimization. PdfNetStack is an MIT licensed .NET library centred on lossless compression and editing. Since 1.1 it also creates documents, fills forms, signs documents, writes PDF/A, extracts text and converts HTML to PDF. This page sets the two side by side. ## What each does well **iText.** Its README describes "a high-performance, battle-tested library that allows you to create, adapt, inspect and maintain PDF documents": creation with a layout engine, manipulation such as merging, digital signing, form creation and editing, PDF/A and PDF/UA, FIPS compliant cryptography, barcodes and SVG ([repository](https://github.com/itext/itext-dotnet)). Add-ons extend it: pdfHTML converts HTML and CSS to PDF, pdfOCR performs OCR, pdfSweep redacts, pdfCalligraph adds scripts such as Arabic, Hebrew and Thai, pdfOptimizer reduces file size and pdfXFA flattens XFA forms ([repository](https://github.com/itext/itext-dotnet)). pdfOptimizer has preset profiles, one of them lossless, and can revert a file or image that grew ([pdfOptimizer](https://itextpdf.com/products/compress-pdf-pdfoptimizer)). Text extraction is part of the kernel API ([PdfTextExtractor](https://api.itextpdf.com/iText/dotnet/9.8.0/classi_text_1_1_kernel_1_1_pdf_1_1_canvas_1_1_parser_1_1_pdf_text_extractor.html)). **PdfNetStack.** By default it makes existing PDFs smaller without changing a rendered pixel, and proves every output equivalent to its source before returning it ([compression](https://www.jadev-corp.com/guides/compress-pdf/), [fidelity](https://www.jadev-corp.com/concepts/fidelity/)). Version 1.0 edits, merges, splits and encrypts documents. Version 1.1 adds a drawing canvas, embedded OpenType fonts with Arabic and right to left shaping, forms, PAdES B-B to B-LTA signatures, PDF/A output, text and image extraction, HTML and CSS to PDF, OCR and opt-in lossy image options. Version 1.2 adds hyphenation and a Tesseract OCR engine. All three versions are on NuGet. ## Licensing in plain words - **iText.** Dual licensed: AGPL-3.0 or a commercial license ([repository](https://github.com/itext/itext-dotnet)). iText's AGPL page says: "You may not deploy it on a network without disclosing the full source code of your own applications under the AGPL license." The same page says you must disclose changes to iText, include the iText copyright and the AGPL license in output file metadata, and keep the producer line in every PDF created or manipulated with iText. "If you cannot comply with the AGPLv3 terms, we offer commercial licensing options" ([iText AGPL page](https://itextpdf.com/how-buy/AGPLv3-license)). The pdfHTML package on NuGet also gives the AGPL as its license ([NuGet](https://www.nuget.org/packages/itext.pdfhtml/6.3.4)). - **PdfNetStack.** MIT for its code, in every package. The packages that bundle the OCR models (`PdfNetStack.Ocr.Models`, `PdfNetStack.Ocr.Tool`) add Apache-2.0 for the models, and `PdfNetStack.Hyphenation` keeps the permissive terms of each pattern set, listed in its license file. Read each license yourself, or ask your counsel, before you decide. This page is not legal advice. ## Features | | PdfNetStack | iText Core 9.8.0 and add-ons | |---|---|---| | License | MIT | AGPL-3.0 or commercial | | Targets | .NET 8 and 10, .NET Standard 2.0 | .NET Framework 4.6.1, .NET Standard 2.0 | | Create documents | 1.1: canvas, fonts, paragraphs | Yes, with a layout engine | | Edit and merge | 1.0 | Yes | | Compress existing PDFs | 1.0, lossless by default; 1.1 adds opt-in lossy image options | pdfOptimizer add-on, with a lossless profile | | Equivalence check of the output | Yes, on by default | Not listed | | Forms | 1.1: create, fill, flatten | Yes | | XFA forms | No | pdfXFA add-on flattens them | | Digital signatures | 1.1: PAdES B-B to B-LTA, sign and validate | Yes | | PDF/A | 1.1: PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e, 4f | Yes | | PDF/UA | Not claimed; tagged PDF from HTML (1.1) | Yes | | Text extraction | 1.1 | Yes | | HTML to PDF | 1.1, `PdfNetStack.Html` | pdfHTML add-on | | OCR | 1.1: `PdfNetStack.Ocr` package; 1.2: Tesseract engine | pdfOCR add-on | | Redaction | No | pdfSweep add-on | | Arabic and right to left text | 1.1, built in | pdfCalligraph add-on | | Barcodes | No | Yes | Sources for the iText column: [repository](https://github.com/itext/itext-dotnet), [NuGet](https://www.nuget.org/packages/itext/9.8.0) (target frameworks of the package), add-ons on NuGet ([pdfHTML](https://www.nuget.org/packages/itext.pdfhtml), [pdfOCR](https://www.nuget.org/packages/itext.pdfocr.tesseract4), [pdfSweep](https://www.nuget.org/packages/itext.pdfsweep), [pdfCalligraph](https://www.nuget.org/packages/itext.pdfcalligraph)), [pdfOptimizer](https://itextpdf.com/products/compress-pdf-pdfoptimizer), [pdfXFA](https://itextpdf.com/products/flatten-pdf-pdfxfa), [PdfTextExtractor](https://api.itextpdf.com/iText/dotnet/9.8.0/classi_text_1_1_kernel_1_1_pdf_1_1_canvas_1_1_parser_1_1_pdf_text_extractor.html). "Not listed" means the feature is not in the product's own documentation on the check date. ## When to pick iText - You need a feature PdfNetStack does not have: redaction, PDF/UA, XFA flattening or barcodes. - The AGPL fits your project, or a commercial license suits you. - You share code or knowledge with a Java team, since iText Core also exists for Java ([iText for Java](https://github.com/itext/itext-java)). ## When to pick PdfNetStack - You want MIT licensing, with no source disclosure or producer line obligations. - You need to shrink existing PDFs losslessly, with a proof that the output matches the input. - You want Arabic and right to left shaping, HTML to PDF, text extraction and OCR from one project under permissive licenses. ## Related - [Compare overview](https://www.jadev-corp.com/compare/) - [Digital signatures](https://www.jadev-corp.com/guides/signatures/) - [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/itext/ --- # PdfNetStack and PdfPig compared > PdfPig next to PdfNetStack for reading text and content from PDFs in .NET: strengths, licenses, features, and when to pick PdfPig. PdfPig is an Apache-2.0 licensed .NET library for reading text and content from PDF files. PdfNetStack is an MIT licensed .NET library centred on lossless compression and editing, with text and image extraction since 1.1. This page sets the two side by side. ## What each does well **PdfPig.** Its README says it "supports reading text and content from PDF files. It also supports basic PDF file creation." It gives each letter with its location, width, font size and font name, words through pluggable word extractors, and document layout analysis tools for page segmentation and reading order. It reads images, annotations, hyperlinks, bookmarks, form fields and embedded files, opens password protected documents, and merges files ([repository](https://github.com/UglyToad/PdfPig)). Images come with their placement and size, and common image types convert to PNG ([wiki](https://github.com/UglyToad/PdfPig/wiki/Images)). Documents it creates can target PDF/A-1a, 1b, 2a, 2b, 3a or 3b ([release notes](https://github.com/UglyToad/PdfPig/releases/tag/v0.1.2), [source](https://github.com/UglyToad/PdfPig/blob/v0.1.16/src/UglyToad.PdfPig/Writer/PdfAStandard.cs)). The 0.1.16 NuGet package targets .NET Framework 4.6.2 and 4.7.1, .NET Standard 2.0 and 2.1, and .NET 6, 8 and 9 ([NuGet](https://www.nuget.org/packages/PdfPig/0.1.16)). The README on the main branch also describes incremental updates, composite TrueType fonts for text with many distinct characters such as Chinese, Japanese or Korean, and embedding files in created documents. Those are in the 0.1.17 alpha prereleases on NuGet, not in 0.1.16. **PdfNetStack.** It makes existing PDFs smaller without changing a rendered pixel and proves every output equivalent to its source before returning it ([compression](https://www.jadev-corp.com/guides/compress-pdf/)). It edits, merges, splits and encrypts documents (1.0). Version 1.1 adds text extraction (page text in reading order with column detection, words and letters with bounds, fonts and sizes), image extraction with decoding to pixels, OCR for scans, and the creation features: a canvas, embedded fonts, forms, signatures, PDF/A and HTML to PDF. Version 1.2 adds hyphenation and a Tesseract OCR engine. All three versions are on NuGet. ## Licensing in plain words - **PdfPig.** Apache-2.0 ([repository](https://github.com/UglyToad/PdfPig), [NuGet](https://www.nuget.org/packages/PdfPig/0.1.16)). A permissive license with an explicit patent grant and a requirement to keep notices ([license text](https://www.apache.org/licenses/LICENSE-2.0)). - **PdfNetStack.** MIT. A permissive license that requires keeping the copyright and license notice. Both allow commercial use. Read each license yourself before you decide. This page is not legal advice. ## Features | | PdfNetStack | PdfPig 0.1.16 | |---|---|---| | License | MIT | Apache-2.0 | | Letters with position, font and size | 1.1 | Yes | | Words | 1.1 | Yes, with several word extractors | | Page text in reading order | 1.1, with column detection | Layout analysis and reading order tools | | Images | 1.1: placement, resolution, decoding to pixels and PNG | Placement, size, PNG for common types | | Read annotations, bookmarks, form fields | 1.1 for annotations and forms, 1.0 for bookmarks | Yes, read only | | Fill and flatten forms | 1.1 | No, forms are read only | | Create documents | 1.1: canvas, embedded OpenType fonts, any script the font covers | Basic: Standard 14 and TrueType fonts, ASCII text | | Edit existing documents | 1.0: pages, metadata, bookmarks | Very limited changes, per its README | | Merge | 1.0 | Yes | | Encryption | 1.0: read and write | Opens password protected files | | PDF/A output | 1.1: PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e, 4f | PDF/A-1a, 1b, 2a, 2b, 3a, 3b for documents it creates | | OCR of scans | 1.1, add-on packages | Not listed | | Lossless compression, every output verified | 1.0 | Not listed | | Signatures, HTML to PDF | 1.1 | Not listed | Sources for the PdfPig column: [repository](https://github.com/UglyToad/PdfPig), [wiki](https://github.com/UglyToad/PdfPig/wiki), [release notes](https://github.com/UglyToad/PdfPig/releases), [NuGet](https://www.nuget.org/packages/PdfPig/0.1.16). "Not listed" means the feature is not in the product's own documentation on the check date. ## When to pick PdfPig - You only need to read text and content, and want a library focused on that. - You want its document layout analysis tools, such as page segmentation and reading order detectors, to build your own extraction pipeline. - You prefer the Apache-2.0 license and its explicit patent grant. ## When to pick PdfNetStack - You need to change documents as well as read them: fill forms, edit pages, encrypt, sign, or compress losslessly with a proof. - You need to create documents with text in any script the font covers, including Arabic and right to left (1.1). - You need to make scanned pages searchable with OCR (1.1, Tesseract engine in 1.2). ## Related - [Compare overview](https://www.jadev-corp.com/compare/) - [Extract text](https://www.jadev-corp.com/guides/extract-text/) - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/) Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/pdfpig/ --- # PdfNetStack and PDFsharp compared > PDFsharp and MigraDoc next to PdfNetStack for .NET PDF work: what each does well, licenses in plain words, features, and when to pick PDFsharp. PDFsharp and MigraDoc are long standing .NET libraries for creating PDF documents. PdfNetStack is an MIT licensed .NET library centred on lossless compression and editing. Since 1.1 it also draws, fills forms, signs, extracts text and converts HTML to PDF. This page sets the two side by side. ## What each does well **PDFsharp and MigraDoc.** PDFsharp "creates and processes PDF documents on the fly" and draws pages "using drawing routines known from GDI+". MigraDoc is "a document generator based on a document object model": you add paragraphs, tables, charts and sections, and it does the layout and creates page breaks, with PDF or RTF output ([documentation](https://docs.pdfsharp.net/)). The feature list includes modifying, merging and splitting existing files, 40 and 128 bit encryption and AES 256 bit encryption, and TrueType fonts and OpenType fonts with TrueType outlines ([features](https://docs.pdfsharp.net/PDFsharp/Overview/Features.html)). Digital signatures based on CMS arrived in PDFsharp 6.2.0 ([signatures](https://docs.pdfsharp.net/PDFsharp/Topics/PDF-Features/Signatures.html)). The documentation has articles on creating PDF/A documents ([PDF/A](https://docs.pdfsharp.net/PDFsharp/Topics/PDF-Features/Archiving.html)) and PDF/UA documents ([PDF/UA](https://docs.pdfsharp.net/PDFsharp/Topics/PDF-Features/Accessibility.html)). The libraries target .NET 8, 9 and 10, .NET Framework 4.6.2 and .NET Standard 2.0 ([documentation](https://docs.pdfsharp.net/)). **PdfNetStack.** It makes existing PDFs smaller without changing a rendered pixel and proves every output equivalent to its source before returning it ([compression](https://www.jadev-corp.com/guides/compress-pdf/), [fidelity](https://www.jadev-corp.com/concepts/fidelity/)). Version 1.0 edits, merges, splits and encrypts documents. Version 1.1 adds a drawing canvas, embedded OpenType fonts with shaping for Arabic and right to left text, paragraph layout, forms, PAdES signatures, PDF/A output, text and image extraction, and HTML to PDF. Version 1.2 adds hyphenation. All three versions are on NuGet. ## Licensing in plain words - **PDFsharp and MigraDoc.** From version 6.5.0 and 7.0.0 Preview 2, "PDFsharp and MigraDoc are published under a Dual License consisting of AGPL and a commercial license" ([license](https://docs.pdfsharp.net/General/License/License.html)). The 6.5.0 NuGet packages carry the license expression `AGPL-3.0-only` ([PDFsharp](https://www.nuget.org/packages/PDFsharp/6.5.0), [PDFsharp-MigraDoc](https://www.nuget.org/packages/PDFsharp-MigraDoc/6.5.0)). Version 6.2.4, every 6.x version before it and the 7.0.0-preview-1 prerelease are listed on NuGet under MIT ([PDFsharp 6.2.4](https://www.nuget.org/packages/PDFsharp/6.2.4)). Check the version you depend on. - **PdfNetStack.** MIT, for every package. On .NET 8 and later the core package has no dependencies. Read each license yourself, or ask your counsel, before you decide. This page is not legal advice. ## Features | | PdfNetStack | PDFsharp and MigraDoc 6.5.0 | |---|---|---| | License | MIT | AGPL-3.0 or commercial | | Targets | .NET 8 and 10, .NET Standard 2.0 (.NET Framework 4.6.2 and later) | .NET 8, 9 and 10, .NET Framework 4.6.2, .NET Standard 2.0 | | Native binaries in the package | None | None | | Create documents in code | 1.1: canvas, fonts, paragraphs | Yes: PDFsharp drawing, MigraDoc document model | | Document model with automatic page breaks | No; HTML and CSS through `PdfNetStack.Html` (1.1) | Yes, MigraDoc | | RTF output | No | Yes, MigraDoc | | Modify, merge, split | 1.0 | Yes | | Lossless compression, every output verified | 1.0 | Not listed | | Encryption | 1.0: AES-256 and AES-128 written; RC4, AES-128 and AES-256 read | Yes: 40 and 128 bit, AES 256 bit | | Digital signatures | 1.1: PAdES B-B to B-LTA, sign and validate | Yes, CMS based, since 6.2.0 | | PDF/A | 1.1: PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e, 4f | Yes, for new documents | | Forms | 1.1: create, fill, flatten | Limited AcroForm support, per its FAQ | | Text extraction | 1.1: text, words, letters | Low level only, per its FAQ | | HTML to PDF | 1.1, `PdfNetStack.Html` | Not out of the box, per its FAQ | Sources for the PDFsharp column: [documentation](https://docs.pdfsharp.net/), [features](https://docs.pdfsharp.net/PDFsharp/Overview/Features.html), [signatures](https://docs.pdfsharp.net/PDFsharp/Topics/PDF-Features/Signatures.html), [PDF/A](https://docs.pdfsharp.net/PDFsharp/Topics/PDF-Features/Archiving.html), [FAQ](https://docs.pdfsharp.net/PDFsharp/Overview/FAQ.html), [license](https://docs.pdfsharp.net/General/License/License.html), [NuGet](https://www.nuget.org/packages/PDFsharp/6.5.0) and the files in that package. "Not listed" means the feature is not in the product's own documentation on the check date. ## When to pick PDFsharp and MigraDoc - You want a document object model that lays out paragraphs, tables and charts and breaks pages for you, from code, without HTML and CSS. MigraDoc does that. - You need RTF as well as PDF from the same document. - Your project already uses PDFsharp and the license of the version you use works for you. ## When to pick PdfNetStack - You need to shrink existing PDFs losslessly, with a proof that the output matches the input. - You want MIT licensing for every package. - You need form filling, text extraction or HTML to PDF from the same library (1.1). ## Related - [Compare overview](https://www.jadev-corp.com/compare/) - [Compress PDF files](https://www.jadev-corp.com/guides/compress-pdf/) - [Edit pages](https://www.jadev-corp.com/guides/edit-pages/) Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/pdfsharp/ --- # PdfNetStack and QuestPDF compared > QuestPDF next to PdfNetStack for generating and processing PDFs in .NET: strengths, licenses in plain words, features, and when to pick QuestPDF. QuestPDF is a .NET library for generating PDF documents from a fluent C# API. PdfNetStack is an MIT licensed .NET library centred on lossless compression and editing. Since 1.1 it also draws, fills forms, signs, extracts text and converts HTML to PDF. This page sets the two side by side. ## What each does well **QuestPDF.** Its README describes a library to "design documents the way you design software: with clean, maintainable, strong-typed C# code", on a layout engine "built specifically for PDF generation" with "automatic pagination". It lists page setup, headers, footers, tables, rows, columns and lists; document operations (merging documents, extracting pages, attaching files, extending metadata, overlays and underlays, optimizing for the web); encryption with access limits; PDF/A, PDF/UA-1 and EN 16931 e-invoicing (ZUGFeRD and Factur-X); rich text, fonts, images, SVG, and right to left text. A companion app gives a live preview with hot reload ([repository](https://github.com/QuestPDF/QuestPDF)). It runs on Windows, Linux and macOS, on .NET 6 and later and .NET Framework 4.6.2 and later ([repository](https://github.com/QuestPDF/QuestPDF)). **PdfNetStack.** It makes existing PDFs smaller without changing a rendered pixel and proves every output equivalent to its source before returning it ([compression](https://www.jadev-corp.com/guides/compress-pdf/), [fidelity](https://www.jadev-corp.com/concepts/fidelity/)). Version 1.0 edits, merges, splits and encrypts documents. Version 1.1 adds a drawing canvas, embedded OpenType fonts, paragraph layout, forms, PAdES signatures, PDF/A output, Factur-X and ZUGFeRD invoices, text and image extraction, and HTML and CSS to PDF. Version 1.2 adds hyphenation. All three versions are on NuGet. ## Licensing in plain words - **QuestPDF.** A free Community License and paid Professional and Enterprise licenses. In the license's own plain language summary, "QuestPDF is free for individuals and businesses with annual gross revenue under USD 1,000,000, and for charitable organisations, academic institutions, and open-source projects. Public-sector entities and publicly traded companies are not eligible, regardless of revenue." An organization that does not qualify "must obtain a paid license before production use". The Community License is source-available, not an OSI approved open source license. The license selection guide gives the exact categories and conditions (version 3.0, effective 6 July 2026: [Community License](https://www.questpdf.com/license/community.html), [license selection guide](https://www.questpdf.com/license/guide.html)). - **PdfNetStack.** MIT, for every package, with no revenue or organization conditions. Read each license yourself, or ask your counsel, before you decide. This page is not legal advice. ## Native code The QuestPDF 2026.9.2 NuGet package ships two native libraries, `questpdf_skia` and `questpdf_qpdf`, for Windows (x64, x86, Arm64), Linux (x64, Arm64, musl x64) and macOS (x64, Arm64) ([NuGet](https://www.nuget.org/packages/QuestPDF/2026.9.2), package contents and release notes). Version 2024.3.0 replaced SkiaSharp with "a custom native layer built on top of Skia" ([release notes](https://github.com/QuestPDF/QuestPDF/releases/tag/2024.3.0)), and the document operations "are created using the qpdf library" ([document operations](https://www.questpdf.com/concepts/document-operations.html)). The PdfNetStack core, `PdfNetStack.Html` and `PdfNetStack.Hyphenation` are managed code only, and publish with Native AOT and trimming on .NET 8 and .NET 10. Native code comes only with the optional `PdfNetStack.Ocr` add-on, through ONNX Runtime. `PdfNetStack.Ocr.Tesseract` (1.2) leaves those libraries out and runs a `tesseract` executable you install yourself. ## Features | | PdfNetStack | QuestPDF 2026.9.2 | |---|---|---| | License | MIT | Community License (free under conditions) or paid | | Native libraries | None in the core | Skia based layer and qpdf, per platform | | Generate documents in code | 1.1: canvas, fonts, paragraphs | Yes, fluent layout API with automatic pagination | | HTML and CSS to PDF | 1.1, `PdfNetStack.Html` | No, documents are built in code | | Merge, extract pages, overlays | 1.0 for pages, 1.1 for overlays | Yes | | Lossless compression, every output verified | 1.0 | Not listed | | Encryption | 1.0: AES-256 and AES-128 written | Yes: 40, 128 and 256 bit | | PDF/A | 1.1: PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e, 4f | PDF/A-2b, 2u, 2a, 3b, 3u, 3a | | PDF/UA | Not claimed; tagged PDF from HTML (1.1) | PDF/UA-1 | | E-invoice metadata (ZUGFeRD, Factur-X) | 1.1: invoice attachment and XMP metadata with PDF/A-3 | Yes, EN 16931 | | Forms | 1.1: create, fill, flatten | Not listed | | Digital signatures | 1.1: PAdES B-B to B-LTA | Not listed | | Text extraction | 1.1 | Not listed | Sources for the QuestPDF column: [repository](https://github.com/QuestPDF/QuestPDF), [features overview](https://www.questpdf.com/features-overview.html), [document operations](https://www.questpdf.com/concepts/document-operations.html), [NuGet](https://www.nuget.org/packages/QuestPDF/2026.9.2) and the files in that package, [license selection guide](https://www.questpdf.com/license/guide.html). "Not listed" means the feature is not in the product's own documentation on the check date. ## When to pick QuestPDF - You build reports and invoices in C# and want a layout API that pages content for you, with a live preview while you design. - You need PDF/UA-1 output, or the accessible PDF/A levels 2a and 3a. - Your organization qualifies for the Community License, or a paid license suits you. ## When to pick PdfNetStack - You need to shrink or edit existing PDFs, with a proof that the output matches the input. - You want MIT licensing with no conditions, or managed code only, for example for Native AOT. - You prefer HTML and CSS templates, or need forms, signatures or text extraction (1.1). ## Related - [Compare overview](https://www.jadev-corp.com/compare/) - [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) - [PDF/A](https://www.jadev-corp.com/guides/pdf-a/) Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/questpdf/ --- # PdfNetStack and wkhtmltopdf compared > wkhtmltopdf next to PdfNetStack.Html for HTML to PDF in .NET: project status, licenses, what each renders, and when wkhtmltopdf still fits. wkhtmltopdf is a command line tool that renders HTML to PDF with the Qt WebKit engine. `PdfNetStack.Html` converts HTML and CSS to PDF with a layout engine written in C#. It shipped in PdfNetStack 1.1 and is on NuGet. ## What each does well **wkhtmltopdf.** wkhtmltopdf and wkhtmltoimage are command line tools that render HTML into PDF and image formats "using the Qt WebKit rendering engine". They run headless, without a display, and there is also a C library ([website](https://wkhtmltopdf.org/)). Pages may run JavaScript, which is on by default. Headers and footers can be text or HTML documents with page numbers, headings become PDF bookmarks, and a table of contents can be generated from the headings ([usage](https://wkhtmltopdf.org/usage/wkhtmltopdf.txt)). **PdfNetStack.Html.** A layout engine in C#, no browser: the CSS cascade, block, inline, table, flex and grid layout, floats and positioning, paged media with `@page`, margin boxes, named pages and running headers and footers, repeated table headers, tagged PDF for accessibility, web fonts, and right to left and Arabic text. Since 1.2, `hyphens: auto` hyphenates with the patterns of the `PdfNetStack.Hyphenation` package. Nothing is fetched from the network unless you pass a resource loader ([HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/)). The result is a `PdfDocument` that can be signed, encrypted or saved as PDF/A with the rest of PdfNetStack. ## Project status - The wkhtmltopdf repository was "archived by the owner on Jan 2, 2023" and is read only ([repository](https://github.com/wkhtmltopdf/wkhtmltopdf)). - The current stable series is 0.12.6, released in June 2020 ([downloads](https://wkhtmltopdf.org/downloads.html)). - Its status page explains that "QtWebKit was eventually deprecated in 2015 and removed in 2016", and advises: "Do not use wkhtmltopdf with any untrusted HTML" ([status page](https://wkhtmltopdf.org/status.html)). ## Licensing in plain words - **wkhtmltopdf.** LGPL-3.0 ([repository](https://github.com/wkhtmltopdf/wkhtmltopdf), [website](https://wkhtmltopdf.org/)). - **PdfNetStack.Html.** MIT, like the rest of PdfNetStack. Read each license yourself before you decide. This page is not legal advice. ## Features | | PdfNetStack.Html | wkhtmltopdf | |---|---|---| | License | MIT | LGPL-3.0 | | How it runs | Managed .NET code in your process | A native command line program, or its C library | | Engine | C# layout engine | Qt WebKit | | JavaScript | No | Yes, on by default | | Headers and footers with page numbers | CSS margin boxes and running elements | Text, or HTML header and footer documents | | Bookmarks from headings | Yes, on by default | Yes, on by default | | Printed table of contents with page numbers | No: `target-counter()` and `leader()` are not supported | Generated from headings | | Flexbox and grid | Yes | Not listed | | Tagged PDF | Yes, on by default | Not listed | | Untrusted HTML | No network without a loader, files only under the base folder, bounded parsing and layout | Status page advises against it | | HTML to images | No | Yes, wkhtmltoimage | | Latest release | 1.2.0, November 2026 | 0.12.6, June 2020 | | Maintained | Yes | Repository archived on 2023-01-02 | Sources for the wkhtmltopdf column: [website](https://wkhtmltopdf.org/), [repository](https://github.com/wkhtmltopdf/wkhtmltopdf), [status page](https://wkhtmltopdf.org/status.html), [downloads](https://wkhtmltopdf.org/downloads.html), [usage](https://wkhtmltopdf.org/usage/wkhtmltopdf.txt). "Not listed" means the feature is not in the product's own documentation on the check date. ## When to pick wkhtmltopdf - Your templates are tuned to WebKit's rendering and already produce the output you want, and you convert only HTML you control. Its status page also suggests running it under a Mandatory Access Control system such as AppArmor or SELinux. - Your templates need JavaScript to build the page. `PdfNetStack.Html` does not run scripts. For sites that use dynamic JavaScript, the wkhtmltopdf status page itself suggests puppeteer or one of its wrappers; see [headless Chromium](https://www.jadev-corp.com/compare/headless-chromium/). - You need images as well as PDF from the same HTML. wkhtmltoimage renders to image formats; `PdfNetStack.Html` writes PDF only. ## When to pick PdfNetStack.Html - You want a maintained engine that runs inside your .NET process, with no native program to install or update. - You convert HTML you do not fully trust. - You want paged media from CSS: margin boxes, running elements, named pages and repeated table headers, or tagged PDF. ## Related - [Compare overview](https://www.jadev-corp.com/compare/) - [HTML to PDF](https://www.jadev-corp.com/guides/html-to-pdf/) - [Paged media](https://www.jadev-corp.com/guides/html-to-pdf/paged-media/) Product names are trademarks of their respective owners, mentioned for reference only, without affiliation or endorsement. Checked on 2026-11-12. Source: https://www.jadev-corp.com/pdfnetstack/compare/wkhtmltopdf/ --- # Roadmap > What PdfNetStack has released, what the next release contains, what comes after it, and what is out of scope for the project. > **Synced from the repository:** This page is generated from [docs/ROADMAP.md](https://github.com/jams4code/pdf.netstack/blob/main/docs/ROADMAP.md) at every build. PdfNetStack is a PDF library and nothing else. Work is ordered by value per unit of risk. Every item ships with tests, and anything that is not lossless sits behind an option that is off by default. ## Released **1.0** (on NuGet): the lossless optimizer with equivalence verification, document editing (open, create, merge, split, import, reorder, rotate and remove pages, document info, bookmarks), encryption (reads RC4, AES-128 and AES-256; writes AES-128 and AES-256 with permissions), and the `pdfnetstack` command line tool. **1.1** (on NuGet): | Area | Scope | |---|---| | Object model | Public `PdfObject` types, `Catalog`, `Resolve`, `AddObject`, `GetStreamData`, page dictionaries | | Drawing | Paths, pens, gray, RGB and CMYK colors, opacity, blend modes, gradients, transforms, clipping, on new and existing pages | | Fonts | Standard 14 fonts; TrueType, OpenType CFF, collections, WOFF and WOFF2; subsetting, Type 0 embedding, ToUnicode, kerning; font collections with CSS font matching (width, style and weight ranges); variable fonts at named instances or any axis values, TrueType (gvar, HVAR, MVAR, cvar) and CFF2, embedded as static fonts once per location, and matched as ranges in collections and HTML | | Text layout | Unicode line breaking (UAX #14) and bidi (UAX #9) passing the Unicode 16 suites; an OpenType shaper matching HarfBuzz on the fonts we compare, with Unicode normalization, Arabic, the nine Indic scripts, the Universal Shaping Engine (Southeast Asian, Tibetan, Mongolian and the other complex scripts), Thai and Lao, Khmer, Myanmar and Hangul; paragraphs with wrapping, alignment, justification (kashida for Arabic, opt in), rich text and fallback | | Images | JPEG, PNG, GIF, BMP, TIFF (every page), WebP and SVG placement with fit, alignment and opacity; Form XObjects; pages of other PDFs as overlays, letterheads and n-up | | Annotations | Links, named destinations (read and rename), notes, text markup, free text, stamps, ink, squares, circles, lines with endings, polygons, polylines and file attachments, with appearance streams; flattening of one annotation, a page or a document | | Forms | Creating text, check box, radio, choice, signature and button fields; reading, filling and flattening; fields travel with imported and merged pages; the Acrobat number, percent, date, time and special format actions applied in C# | | Document features | Viewer preferences, page labels, language, custom properties, file attachments | | Signatures and PDF/A | PAdES B-B, B-T, B-LT and B-LTA (RSA and ECDSA): RFC 3161 signature and document timestamps, the document security store with OCSP and CRLs; verification with chain building to trust anchors, validity, key usage and revocation; the X.509 checks of ETSI EN 319 102-1: certificate policies with mappings and constraints (RFC 5280 on the RFC 9618 policy graph), name constraints, and the qualified status QCStatements claim, confirmed against EU trusted lists the application supplies (ETSI TS 119 612 lists with their XML signatures verified, qualification as ETSI TS 119 615 decides it, qualified time stamp services); PDF/A-1b, 2b, 2u, 3b, 3u, 4, 4e and 4f output validated by veraPDF in CI; Factur-X and ZUGFeRD invoices | | Content extraction | Text in reading order with column detection and vertical writing; Adobe's CJK CMaps and CID to Unicode tables; built-in encodings of embedded Type 1 and CFF fonts; letters and words with geometry; images with placement, decoded from JPEG, JPEG 2000, CCITT fax and JBIG2 to PNG | | Optimizer | Opt-in lossy image profile: downsampling by placed resolution, scans included (JPEG 2000 to JPEG, CCITT and JBIG2 kept bilevel with strokes preserved), photographs (raw or JPEG 2000) to JPEG, a perceptual SSIM gate, progressive JPEG; an exhaustive Zopfli style Flate encoder. Bilevel images coded as CCITT Group 4 (ITU-T T.6) with every pixel proven. Lossless structural passes: unused resource pruning, content stream compaction, merging of font subsets, deduplication by decoded content, and opt-in linearized output (fast web view), each proven by the equivalence check | | Scale and performance | Documents read on demand from a file, a seekable stream or a stream that cannot seek (copied to memory or a temporary file), with incremental saving; `PdfLoadOptions` (password, decode and image pixel limits, mode, spool threshold and folder), `OpenAsync` without synchronous reads, `PdfDocument` disposable, the input read once, and `ReleaseUnusedObjects` to keep memory flat while reading page by page; progressive saving (each object written as it is serialized, the cross-reference data last) and `SaveAsync` with asynchronous writes only; a streaming optimizer for files larger than memory, which reads the source on demand, writes through a temporary file and runs the full proof on it (a generated 278 MB file: about 50 MB of live memory against 810 MB in memory, the same bytes out); text extraction (3.8 times) and HTML conversion (1.5 to 1.9 times) profiled and made faster with unchanged output, measured by a benchmark project under `tools/`; JPEG 2000 decoded tile by tile and a band of rows at a time straight into 8 bit pixels, so 600 dpi color scans fit the default decode limit; JBIG2 regions combined a byte at a time, page sized text regions drawn straight onto the page, and bilevel images expanded to pixels by lookup | | Serverless and containers | Native AOT and trimming without warnings for the core, HTML and OCR libraries on linux-x64 and linux-arm64, checked in CI by a smoke app; tests under invariant globalization and tr-TR; robust font directory scanning; bounded, thread safe caches in long lived instances; cold start measured and lazier; AWS Lambda and Azure Functions samples | | Command line | Native AOT executables for linux-x64, linux-arm64, win-x64, osx-x64 and osx-arm64 on every GitHub release, with SHA-256 checksums | | Searchable scans | `DrawInvisibleText` in the core; the `PdfNetStack.Ocr` add-on (PP-OCRv5 on ONNX Runtime, Latin and Arabic) with its models in `PdfNetStack.Ocr.Models`, scans stored in one image or in several (strips, tiles, a text mask over a background), pages scanned sideways or upside down found and read turned back, an `IPdfPageRasterizer` hook for pages an application renders; the `pdfnetstack-ocr` command line tool (`PdfNetStack.Ocr.Tool`, also run as `pdfnetstack ocr`) | | HTML and CSS | `PdfNetStack.Html`: cascade, block, inline, table, flex and grid layout (named lines, dense packing, column flow, subgrid, auto-fit), floats, positioning, paged media with margin boxes, named pages at any depth, running elements, web fonts, variable fonts (`font-weight`, `font-stretch`, `font-style`, `font-optical-sizing` and `font-variation-settings` choosing the instance), tagged PDF | | Evidence | A public benchmark corpus of 77 openly licensed PDFs, listed by URL and SHA-256 in `eng/corpus/manifest.json` and never committed; `tools/corpus_bench.py` measures lossless and lossy optimization (size, time, peak memory), proves every rewrite again, compares extracted text and images and PDFium renders, weekly, on demand and for every version tag in CI, where the report and a comparison with the previous release's report (`tools/corpus_compare.py`: size, time, memory, new and fixed failures) are attached to the GitHub release | **1.2** (on NuGet): | Area | Scope | |---|---| | Hyphenation | Liang's algorithm in the core, opt in through `PdfParagraph.Hyphenator`, with exceptions and limits per language; `PdfNetStack.Hyphenation` with patterns from TeX's hyph-utf8 for English (US and UK), German (reformed spelling), French, Dutch, Spanish, Italian and Portuguese and 21 more sets, all under permissive licenses and read per language on first use; `hyphens: auto` with `lang` in `PdfNetStack.Html` | | Searchable scans | `PdfNetStack.Ocr.Tesseract`: a second `IOcrEngine` that runs an installed `tesseract` executable, one process per image, with any language Tesseract has data for. It drives the executable rather than a .NET wrapper: `docs/OCR.md` has the evidence and the reasons | Languages whose only patterns are under LPPL, GPL, LGPL or MPL (among them Russian, Swedish, Czech, Hungarian, Turkish, Catalan) or give no right to modify (Finnish) stay out, as does German in the 1901 spelling, whose patterns are permissive but not included. ### Corpus results The first corpus run (77 documents, 4,045 pages): the lossless profile rewrote 65 and saved 12.1% (196.6 MiB to 172.9 MiB), the lossy profile 27.2%; the other 12 are signed or encrypted and were left alone. 217 pages rendered by PDFium were identical after the lossless rewrites, text and decoded images read the same before and after, and no rewrite failed its proof. The run found three bugs, fixed with synthetic regression tests: a looping `/Prev` chain that hid the page tree, compaction refusing streams with `%` inside strings, and a JBIG2 work budget too small for page sized symbols. JPEG 2000 images of 600 dpi book scans exceeded the default decode limit when extracted, and JBIG2 and JPEG 2000 extraction was slow on such pages (about a second per page). Both are fixed: a 4812 by 7541 color page now decodes within 256 MiB (about 190 MB at peak, the 109 MB of pixels included, against about 1 GB before), and JBIG2 text pages come out of `PdfPageImage.Decode` four to five times faster. With JPEG 2000, JBIG2 and CCITT images in the lossy profile, a later run saved 29.5% lossy, and 24.0% on the nine scanned documents (4.1% lossless): 37% on a 309 page JBIG2 report, 42% to 88% on CCITT pages. With palettes (`QuantizeImages`) the lossy profile saves 32.1%, and the `Balanced` preset, which keeps every image's resolution, 23.4%. The two Internet Archive books, a JPEG 2000 picture over a JBIG2 soft mask on every page, still gain about 2%, and their JPEG 2000 decoding takes 2 to 4 minutes per book. ## Next **Evidence** - Publish the comparison with qpdf, MuPDF and Ghostscript. `tools/compare_optimizers.py` runs it in the corpus workflow and scores size, rendering (SSIM) and the structure each tool keeps; the numbers go in the docs once a few weekly runs agree. **Compression** - Better JPEG coding. A rounding deadzone gave 0.4% to 3% at equal rendered quality in a test, not enough to loosen the exact requantization proof. Trellis quantization is untried. **OCR** - Running the Tesseract engine against versions other than 5.3.4, and on Windows. ## Out of scope - Rendering pages to images. - Running JavaScript or embedding a browser engine. - Wrappers around external tools in the core packages, which stay pure managed code. Native code and external programs are allowed only in optional add-ons, such as ONNX Runtime and the Tesseract engine behind OCR. - Cloud or hosting integrations. PdfNetStack works on bytes and streams; where they come from is up to the application. The samples in `samples/serverless` show the wiring for two hosts. Source: https://www.jadev-corp.com/pdfnetstack/roadmap/ --- # Changelog > Every change to PdfNetStack by release, newest first: what is in main and not yet on NuGet, and what each published version added. > **Synced from the repository:** This page is generated from [CHANGELOG.md](https://github.com/jams4code/pdf.netstack/blob/main/CHANGELOG.md) at every build. ## 1.2.0 (2026-11-10) - `PdfOptimizerOptions.MaxBilevelImageDpi` (`--max-bilevel-dpi`) gives bilevel images (raw, CCITT, JBIG2) their own downsampling resolution, in place of `MaxImageDpi`. Null keeps the old behavior. `Smallest` now sets it to 300: at 150 dpi one bit per pixel made scanned text ragged, which the image quality gate does not judge, since a bilevel reduction is exact by construction. - OCR: ONNX Runtime 1.31.1. One build property now sets its version everywhere it is named. - Hyphenation, off by default, by Liang's algorithm as TeX defines it. `PdfParagraph.Hyphenator` takes a `PdfHyphenator`: words then break at the line's end where the language allows, showing a hyphen, while `PdfParagraph.Text` keeps them whole and extracted text marks the hyphen with U+00AD. `PdfHyphenator.Create` takes patterns and exceptions in TeX's formats, `WithExceptions` adds words, and `MinWordLength`, `MinLeft`, `MinRight` and `HyphenateAllCaps` set the limits. `FindBreaks` and `InsertSoftHyphens` work on text of your own. Left alone: words with a soft hyphen, words joined to a digit or holding a capital past the first letter, words of all capitals by default, web and e-mail addresses and paths, words over 64 letters. A point inside a ligature is skipped, and fonts without a hyphen glyph do not hyphenate. Results do not depend on the current culture. - `PdfNetStack.Hyphenation`, a new optional package with patterns from TeX's hyph-utf8 for `en-us`, `en-gb`, `de` (reformed spelling), `fr`, `nl`, `es`, `it`, `pt`, `af`, `be`, `bg`, `cy`, `da`, `el-monoton`, `el-polyton`, `et`, `eu`, `ga`, `gl`, `hr`, `la`, `lt`, `nb`, `nn`, `pl`, `sk`, `sl`, `sq` and `uk`, all under permissive licenses, loaded one language at a time on first use by `PdfHyphenationPatterns.ForLanguage`. Languages whose only patterns have other licenses (among them `ru`, `sv`, `cs`, `hu`, `tr`, `ca` and `fi`) are left out; `eng/check-licenses.ps1` now checks every set against the allowlist. - `hyphens: auto` in `PdfNetStack.Html` hyphenates text in the language of the nearest `lang` attribute when `HtmlConverterOptions.Hyphenators` finds patterns for it, with `PdfHyphenationPatterns.ForLanguage` for instance. Without the option nothing changes, and `auto` behaves like `manual`. Min-content sizes count the points, so narrow table columns can hyphenate instead of overflowing. Words are judged as written: text under `text-transform: uppercase` hyphenates like its source words, while text written in capitals stays whole. - `text-transform: uppercase` writes the German sharp s as SS, as CSS Text 3 asks, with or without hyphenation: `Straße` now reads `STRASSE`, where it read `STRAßE`. Other characters whose full upper case takes several letters keep their simple mapping. - Fixed: justifying a line that ended at a soft hyphen could widen the hyphen along with the spaces, when the run the hyphen belonged to ended with a space. - New package `PdfNetStack.Ocr.Tesseract`: `TesseractOcrEngine`, an `IOcrEngine` that runs an installed `tesseract` executable (versions before 4 are refused; CI runs 5.3.4), so `PdfOcr` can read any language Tesseract has data for and run where ONNX Runtime has no build. One process per image, started without a shell: the pixels go in on standard input as a bitmap, with no temporary files, and word boxes and confidence come back as TSV on standard output, parsed as untrusted text. Language names are checked before they reach a command line. A missing executable or missing language data fails at `Load`, saying where to get it. `Timeout`, `MaxOutputBytes` and cancellation kill the process, and `MaxConcurrentProcesses` limits how many run at once. Tesseract and its data are installed separately, and ONNX Runtime's native libraries stay out: an application using only this engine publishes 3 MB of libraries instead of 241 MB. Pages scanned sideways are not detected with this engine. A CI job runs it against Tesseract from the Ubuntu packages on x64 and arm64. ## 1.1.0 (2026-11-05) - `QuantizeImages` (`--quantize`) reduces raw 8 bit RGB images to a palette of at most 256 colors in their own color space, for the drawings and screenshots where JPEG rings around fine labels and barely saves anything. Each image gets the smallest palette, of 256, 64, 16 or 4 colors, that keeps `MinImageSsim`, or 0.97 without a gate, and no more than 8 sample steps of mean change with at most 1% of pixels moved by over 32, which catches a hue drift luminance cannot see. A color holding most of its cluster stays exact, so white stays white. With `ConvertImagesToJpeg` the smaller candidate wins image by image. The proof expands the palette and checks the same bounds, and `PdfEquivalence.FindDifference` accepts such images when given a minimum SSIM. On a technical manual full of screenshots and diagrams, `JpegQuality = 50` alone saved 18.0% and adding `QuantizeImages` saved 47.9%. `QuantizeImages` alone saved 43.7%, with pages rendered by PDFium closer to the source than with `JpegQuality = 50` alone (mean SSIM 0.9999 against 0.9996). The pass costs CPU: about 2.7 times as long on that file, single threaded. On the public corpus it took the lossy profile from 29.5% to 32.1% with no failure: 15 of the 65 rewritten documents got smaller, up to 45%, and none got larger. - The corpus benchmark measures a third profile, `balanced`, the preset of that name: 23.4% on the public corpus, at full resolution, with no page below its PSNR warning. `--profiles` picks others. - `PdfOptimizerOptions.Balanced` and `PdfOptimizerOptions.Smallest` (`--preset balanced`, `--preset smallest`) are ready sets of lossy options. `Default` stays lossless. - The corpus benchmark also runs for every version tag and attaches its report to the GitHub release, with a comparison against the report of the previous release made by `tools/corpus_compare.py`: savings, time and peak memory per profile, and per document any rewrite that grew by more than 1% of the original, took 1.5 times as long, needed 1.3 times the memory, stopped being rewritten or now fails a check. Reports record each document's SHA-256, so only identical files are compared. - Opening documents with control over memory, for serverless hosts and large files. `PdfLoadOptions` gathers the password, `MaxDecodedStreamBytes`, a new `MaxImagePixels`, the `Mode` (`PdfLoadMode.InMemory` or `OnDemand`), `SpoolThreshold` and `TempDirectory`, taken by new overloads `PdfDocument.Open(string, PdfLoadOptions)`, `Open(Stream, PdfLoadOptions)` and `Open(ReadOnlyMemory, PdfLoadOptions)`; the existing overloads keep working and call them. - The input is read once. `Open(Stream)` copied a stream into a growing buffer and then into an array; now a `MemoryStream` whose buffer is exposed is used without copying, a seekable stream is read into an array of its length (4.0 MB allocated for a 4.0 MB file, was 8.0 MB from a `MemoryStream`), and any other stream is read in pieces joined once. - On demand from a path, the document opens the file itself and keeps it until it is disposed. `PdfDocument` is now `IDisposable`: disposing a document read on demand closes what it opened and makes later use throw `ObjectDisposedException`; the caller's stream is never disposed, and disposing a document in memory does nothing. - A stream that cannot seek (an HTTP request body, a storage download) can be read on demand: `OpenOnDemand` and `Open` copy it to memory up to `SpoolThreshold` (16 MiB) and past it to a temporary file in `TempDirectory` (the system's temporary folder by default; /tmp on AWS Lambda), opened with `DeleteOnClose` on Windows and unlinked once open elsewhere, and deleted when the document is disposed or opening fails. - `PdfDocument.OpenAsync(Stream, PdfLoadOptions?, CancellationToken)` reads the stream with asynchronous reads only, for hosts that forbid synchronous I/O, and deletes any partial copy when cancelled. - `MaxImagePixels` (2^28 by default, the previous fixed cap) refuses an image with more pixels before its pixels are allocated, with `PdfFormatException`, for raw samples, JPEG, JPEG 2000, JBIG2 and CCITT images, by the dictionary's size and by the size the data declares. `PdfImage.Load(byte[], int, long maxPixels)` does the same for JPEG, PNG, GIF, BMP, TIFF and WebP files, with `FormatException`. - `PdfDocument.ReleaseUnusedObjects()` frees the objects a document read on demand has parsed and nothing uses any more; they are parsed again when needed. Objects that differ from the file, and objects still referenced anywhere (found by a full garbage collection), are kept, so no edit is lost. Reading 300 pages that each have their own font kept 1,503 parsed objects and 7.8 MB; releasing every 50 pages kept 301 objects and 1.0 MB. - `PdfDocument.Save(Stream)` writes progressively when `PdfSaveOptions.Optimize` is off: each object, or batch of objects for an object stream, goes to the stream as soon as it is serialized, offsets are counted as bytes go out, and the cross-reference data comes last. Memory beyond the document follows the largest object, not the file; a document opened with `OpenOnDemand` has its stream data copied from the source as it is written. The bytes are the ones `ToArray` returns for the same options. On a generated 278 MB file opened on demand, an unoptimized save peaked at 3 MB of live memory (51 MB working set) instead of 835 MB, and took 0.9 to 1.2 s instead of 1.4 to 5.6 s on a busy machine. With the optimizer on, the default, the file is still built in memory, as the proof needs all of it; the XML docs say so. - Without the optimizer, encryption is applied in the same single pass; the optimized path still encrypts the proven file as a last step. - `PdfDocument.SaveAsync(Stream, PdfSaveOptions?, CancellationToken)` and `SaveAsync(string, ...)`: the same output, written with asynchronous calls only and flushed, for response bodies in ASP.NET Core and Azure Functions isolated, which refuse synchronous IO. Cancellation is observed before each object and by each write; a cancelled or failed save throws, and `SaveAsync(path)` then leaves the file as it was. - `HtmlConverter.ConvertToPdfAsync(html, stream, options, cancellationToken)`: layout on its own thread with no caller thread waiting, then `SaveAsync`. - Streaming optimizer: `PdfOptimizerOptions.Streaming`. The source is read on demand (as `OpenOnDemand` reads it), streams are recompressed a batch at a time (at most 32 MiB of source data) and written straight away, and the rewrite goes to a temporary file: the destination's own temporary file for `OptimizeFileAsync`, otherwise one in the system temporary folder. The proof is the in-memory proof, run on a reader over that file: every reachable object compared, every stream by decoded content. Duplicates are found from what streams decode to before anything is written, so the output can differ from the in-memory rewrite (JPEG files differing only in metadata stay two); on the test documents and the benchmark file it is the same. `OptimizeAsync` reads a `FileStream` or `MemoryStream` in place and copies any other input to a temporary file first, asynchronously; the caller's output is only written asynchronously. Linearization cannot be combined with it. `OptimizationStatistics.Streamed` reports the mode, and `pdfnetstack optimize --streaming` turns it on, without an input size limit. On a generated 278 MB file: 50 MB of live memory (300 to 460 MB working set) against 810 MB (1.4 GB), and no slower (8 to 11 s against 10 to 15 s on a busy machine), measured by `tools/PdfNetStack.MemoryBench`. - Serverless and containers. The core, HTML and OCR libraries publish with Native AOT and trimming with no warnings (`PdfNetStack.Ocr` is now marked AOT compatible too), checked in CI on linux-x64 and linux-arm64 for .NET 8 and .NET 10 by `tests/PdfNetStack.AotSmoke`, which roots all three libraries and then creates, opens, optimizes, extracts text and images from, reads predefined CJK CMaps in and converts HTML to PDF. CI also runs the test suite with invariant globalization and under tr-TR (`PDFNETSTACK_TEST_CULTURE` sets the culture of a test run). - `PdfFontCollection.AddDirectory` and `PdfFontCollection.System` skip a directory that cannot be listed instead of giving up on the whole tree, search at most 16 levels deep without following links to directories (a link cycle used to recurse until the path was too long), and visit files in ordinal order, so the same fonts win on every machine. With no home directory (HOME unset, as in some serverless hosts) the per user font directories are skipped; they used to resolve against the current directory. - Variable font instances are held weakly by their font. A font shared by a long running process, such as one in a `PdfFontCollection` used for every HTML conversion, no longer keeps every location it was ever asked for (each a full font program); an instance stays shared while a document or the application holds it. - Name constraints fail closed without ICU. Under invariant globalization .NET performs no NFKC normalization or IDNA mapping, so a directory string or host name with characters outside ASCII cannot be put in canonical form; it now counts as undecided (excluded, and not permitted) instead of being compared as written, which let full width letters pass an excluded subtree. - Cold start: the syllable grammars of the Khmer, Myanmar and Universal Shaping Engine shapers are built when such text is first shaped, not with the first shaper. The first line of text in an embedded font allocates 360 KB less and, under the JIT, takes about 10% less time. `tools/measure_cold_start.py` measures the first open and save, optimization, text extraction, HTML conversion and embedded font text in a fresh process. - Samples under `samples/serverless`: a Native AOT AWS Lambda function (provided.al2023, arm64) that optimizes PDFs uploaded to S3 and converts HTML to PDF behind a function URL, with a SAM template, and an Azure Functions isolated worker app with an HTTP trigger for HTML and an Event Grid blob trigger for optimization. Both are built in CI. - OCR finds pages scanned sideways or upside down. When most of the lines read on a page do not run left to right (the detector found them tall, or the line orientation model turned them), a copy reduced to 1600 pixels on its long side is read in each of the four orientations and the one with the most confident left to right text wins; the page is then read turned back and the words map back to page space through the turn. `PdfOcrPage.Orientation` says how far the content stood turned; `PdfOcrOptions.DetectOrientation` (on by default) turns it off. Upright pages cost nothing more. Sideways pages now read in line order. - OCR rasterizer hook: `PdfOcrOptions.Rasterizer` takes an `IPdfPageRasterizer` the application supplies, for pages with no scan to decode (text as vector outlines, vector art, encodings not decoded). It renders the page as shown at `RasterizerDpi` (300, at most `MaxOcrDpi`, lowered to fit `MaxImagePixels`); its result is checked for size and shape, and its failures are reported per page. PdfNetStack still does not render; a PDFium based renderer can be plugged in. - `pdfnetstack-ocr`, a command line tool in its own package, `PdfNetStack.Ocr.Tool`: `pdfnetstack-ocr in.pdf out.pdf --languages latin,arabic [--models folder]`, with `--text`, `--json`, `--no-orientation`, `--all-pages`, `--min-confidence`, `--threads` and `--password`. It carries the models and ONNX Runtime's native libraries for Windows, Linux and macOS (a 70 MB package), so `PdfNetStack.Tool` stays small and free of ONNX. `pdfnetstack ocr` runs it when it is installed and says how to install it when it is not. - `eng/check-licenses.ps1` allows a package under `onlyIn` to ship with several named projects. - Signature validation confirms qualified status against EU trusted lists the application supplies; the library still downloads nothing. `PdfTrustedList.Load` reads ETSI TS 119 612 lists (the list of lists and national lists) from bytes or a stream, with DTDs prohibited, at most 32 MiB and 64 levels: scheme information, pointers to other lists with their signing certificates, providers, services with type, status, history, digital identities (certificates, subject key identifiers, subject names), qualifications with their criteria and additional service information. Each list's enveloped XMLDSig signature is verified against the certificates supplied for it, or for a national list against the pointers of a verified list of lists (`Load(xml, listOfLists)`): every reference must digest correctly and one must cover the whole list, identifiers must be unique, and Canonical XML 1.0 and 1.1 and Exclusive XML Canonicalization are implemented in the library (no new dependency), checked against Microsoft's implementation in the tests. RSA PKCS #1 v1.5, RSA-PSS with any salt length (also for id-RSASSA-PSS keys) and ECDSA, with SHA-256, 384 and 512; SHA-1 is refused. The signer must be valid at the signed XAdES signing time, or else at the issue date. - `PdfValidationOptions.TrustedLists` and `TrustedListsAsTrustAnchors`; every signer gets `PdfCertificateValidation.TrustedListQualification` (`Qualification` at the signing time, `AtIssuance`, the deciding `Service`, `Reasons`) as ETSI TS 119 615 decides it: the CA/QC service whose identity is the public key of a CA above the certificate, its status from the history at issuance and at the signing time (granted, or supervised or accredited before eIDAS), the qualifiers its criteria select (key usage, policies, extended key usage, subject attributes, nested lists) and the certificate's QCStatements. Conflicts, criteria or critical extensions the library cannot evaluate, and missing or expired lists give the new `PdfQualification.Indeterminate`. So does a certificate whose own checks did not pass: the lists are then not consulted. Services are matched by public key only: a listed certificate with the same key, or a listed X509SKI equal to the SHA-1 of the certificate's subjectPublicKey (RFC 5280 method 1). The subject key identifier extension a certificate carries is never used. - Trusted list anchors (`TrustedListsAsTrustAnchors`, default true) are restricted. Only the identities of CA/QC and CA/PKC services anchor a chain, and only for a validation time at which the service's entry in force has an approved status (granted, recognised at national level, or before eIDAS under supervision, in cessation or accredited) and no critical extension the library does not interpret. TSA/QTST and TSA services anchor time stamp authority chains under the same rules, never a signer's. OCSP, CRL and other services, and withdrawn, deprecated or never approved ones, anchor nothing. The default stays true: lists are only used when their signature verified against certificates the application supplied, and without anchors the qualification could never be confirmed. A chain that reaches none of these anchors fails, and the problems say what the list names its top certificate as. - Timestamps get `PdfTimestampInfo.QualifiedTimestamp`: Passed only when the authority's certificate checks passed, the token is intact, and a TSA/QTST service granted at the validation time has as its identity the authority's own key, or the key of a CA of its chain that the list names as that TSA/QTST service. Otherwise Failed when no such service was granted, or Indeterminate when the authority's checks did not pass. `tools/PdfNetStack.TrustedListCheck` reads and verifies the live EU lists for a manual check; the tests generate every list and PKI. - JPEG 2000 images decode tile by tile, a band of rows at a time, straight into 8 bit pixels. Only a window of rows per resolution and a few rows of code-blocks per subband are held in 32 bits, so the decode limit has to cover the pixels and those rows instead of 32 bit planes of the whole image: a 600 dpi color page (4812 by 7541, one tile) now decodes within the default 256 MiB, peaking near 190 MB where it needed about 1 GB. Tiled images reuse one tile's buffers. The samples are unchanged, bit for bit. - JPEG 2000 decoding is faster with unchanged output: tier-1 keeps each stripe column's state in one word and skips columns with nothing to code, the MQ decoder renormalizes a byte at a time, rows are lifted with their low and high-pass halves apart and columns several at a time, and the color transform and level shift run in one pass. A 6 megapixel lossy RGB image decodes in about 60% of the time it took; lossless images gain less, being bound by the arithmetic decoder. - JBIG2 regions combine with the page a byte at a time instead of a pixel at a time, and an immediate text region that draws onto the page the same way symbol by symbol (OR onto a white region, AND onto a black one, or REPLACE) is drawn straight onto it with no bitmap of its own. MMR runs fill bytes, and generic regions read their context rows by offset. A synthetic 600 dpi text page decodes about four times faster. - `PdfPageImage.Decode` maps one component images of 8 bits or less (bilevel scans, gray, indexed) through a table of each sample value's pixel instead of converting every pixel, and returns a JPEG 2000 image's 8 bit gray or RGB pixels without copying them. With the JBIG2 changes, a synthetic 600 dpi text page comes out of `Decode` four to five times faster. - `tools/make_jpx_fixtures.py --large` writes a large synthetic page for measuring memory and time; it is not committed. - Optimizer: bilevel images are coded as CCITT Group 4 when that is smaller (`PdfOptimizerOptions.OptimizeBilevelImages`, on by default; `--no-group4` turns it off). It takes 1 bit images stored as samples under any general filters, and CCITT Group 3 data that decodes without a flaw and whose parameters leave nothing to a reader's defaults. The encoder follows ITU-T T.6 and writes the same bytes as libtiff. The proof decodes both versions and requires every pixel to match, and `PdfEquivalence` accepts such a recoding from any tool on the same terms. `OptimizationStatistics.ImagesRecodedAsGroup4` counts them. - Lossy optimizer options reach scanned documents. `MaxImageDpi` reduces bilevel images (raw, CCITT or JBIG2) and keeps them bilevel: each new pixel takes the share of the source that covers it, the less common value wins past half, and a stroke thinner than the new pixels keeps its middle, so text stays legible; the result is Group 4 or Flate. It also reduces gray and RGB JPEG 2000 images, which become JPEG at `JpegQuality` (or lossless samples without it), and `ConvertImagesToJpeg` turns photographic JPEG 2000 images into JPEG. The proof decodes JPEG 2000, JBIG2 and CCITT sources, compares a bilevel output with the same reduction of its source, and allows a JPEG 2000 image without /BitsPerComponent to gain /BitsPerComponent 8. A JBIG2Globals stream no image uses any more is dropped. - Optimizer: every JPEG the lossy options make is checked against the proof's color and coarse scale bounds before it is chosen, so a small image a JPEG would shift too far stays as it was instead of failing the proof of the whole file. - The lossy image pass shares a memory budget of twice `MaxDecodedStreamBytes` between its workers, so a document of large JPEG 2000 scans no longer decodes one per thread at once: peak memory on two scanned books of the benchmark corpus fell from 5.7 and 7.0 GiB to 2.2 and 2.7. - CCITT decoding: a row whose every pixel changes color (a 1 or 2 pixel wide image, for one) no longer ends decoding there. - `tools/scan_bench.py` writes synthetic scans of each kind a scanner produces (1 bit under Flate and LZW, Group 3 fax, Group 4, JBIG2, gray and color JPEG 2000) and measures both optimizer profiles on them. - HTML conversion is 1.5 to 1.9 times faster with the same PDF, byte for byte. On the benchmark samples (fastest of 125 conversions each, on a busy 8 core machine): a one page invoice went from 46 to 31 ms, a one page report from 30 to 20 ms, a 21 page invoice with a 640 row table from 465 to 244 ms, a 15 page report with lists, images, SVG charts and a grid from 110 to 68 ms, and a 52 page text from 316 to 193 ms. The first conversion in a new process is 1.1 to 1.8 times faster. What changed: - `ConvertToPdf` saves without the optimizer's equivalence proof, which read both files back and took about half the optimizer's time: the converter draws all the content itself. New `PdfSaveOptions.VerifyOptimization` (default true) and `HtmlConverterOptions.SaveOptions` (default null: as `PdfSaveOptions.Default` without the proof); set `SaveOptions` to `PdfSaveOptions.Default` to keep the proof. - Each page paints the parts of the document that reach it instead of walking all of it. - Table cells are measured for the column widths and laid out from the same text, fonts, shaping and break opportunities, and collapsed borders read each style once per table. - Shaped runs are kept for the rest of the conversion (up to 256 characters a run and 256K characters in all), so repeated labels and numbers shape once. - OpenType lookups pass over glyphs their first coverage does not list, as HarfBuzz's set digests do; each stage's lookups are worked out once per font. A hostile font's step budget is spent more slowly as a result; output is otherwise the same. - Installed fallback fonts are looked up only when a character needs them, so a document in the generic families never scans the installed fonts: its first conversion in a process went from 352 to 258 ms. - Font subsets are made and compressed side by side when a document is saved. - Less work per character in inline layout: the bidi algorithm is skipped for text below U+0590 in a paragraph that is not right to left (every level is 0 there), fonts are chosen once per run of one style, break opportunities need no sorting, and CSS keywords and colors are read once per value. Attribute values without character references, such as data: URIs, are copied whole. - The optimizer's content analysis reads operators without a string each, and font program merging reads each program once. - Fixed: `ConvertFileToDocument` dropped `MaxPages` when it set the base address. - `tools/PdfNetStack.Benchmarks` has three larger HTML samples (a 21 page invoice, a 15 page report with lists, images, SVG and a grid, a 52 page text); `quick`, `cold` and `samples` take a sample name, `samples` also writes the PDFs and their page counts, and `quick` counts the allocations of every thread. - HTML: absurd sizes no longer exhaust memory. A font size such as 1e300px laid out one line across an unbounded number of pages. Computed lengths are now clamped to a million px either way (percentages to a million percent), font sizes to 10000px, unitless line heights to 1000 and page sizes to 14400 points a side, and `HtmlConverterOptions.MaxPages` (default 10000) ends a conversion that needs more pages with the new `HtmlConversionException`. - Variable fonts in font matching. `PdfFontCollection` counts a variable font for every weight, width and slant its axes cover, and registers its named instances as faces; faces are also found by full or PostScript name. `Find(family, PdfFontQuery)` matches width, then style, then weight with ranges as CSS Fonts 4 does (5.2), and returns the instance at the values found, with `OpticalSize` and `Variations` applied over them (7.2): ital for italic, slnt for oblique, never both. `PdfFontStyle` gives normal, italic or oblique at any angle. Static families match as before, now also by OS/2 width class. - HTML: `@font-face` takes ranges in `font-weight`, `font-stretch` (and `font-width`) and `font-style` (`oblique 0deg 15deg`), and the `font-variation-settings` and `font-named-instance` descriptors. A missing descriptor is `auto`: the face matches as normal and the variation follows the style unclamped. `font-style: oblique `, `font-optical-sizing` (opsz set to the font size in CSS px) and `font-variation-settings` are supported; the `font` shorthand resets the last two. Rules with identical descriptors: the last one wins. - Variable font instances are made once per location and the default location is the font itself, so a document embeds one program per location drawn, however many sizes use it. `@font-face` rules that share a file share its font. A damaged variation table makes matching fall back to the font's default instance. - HTML bounds the variable font instances a document can ask for: axis values snap to a grid (whole units for wght, wdth and opsz, half degrees for slnt, tenths for ital, a hundredth of the range for other axes) and each font makes at most 32 instances per conversion, the nearest one standing in beyond that. Font values that are not finite are dropped and the rest clamped, and a style value that font matching still rejects falls back to the family's default instead of failing the conversion. `font-stretch` accepts only finite percentages. - `PdfFontQuery` checks each value when it is set and copies `Variations`, so an invalid query throws where it is built. - `tools/make_variable_font_fixtures.py` also builds Test-Axes.ttf, a font with wght, wdth, opsz, ital and slnt axes for the matching tests. - PDF/A-1b output (`PdfConformance.PdfA1B`): PDF 1.4 with a classic cross-reference table and no object streams, also when the optimizer runs. Transparency (opacity, soft masks, alpha images, blend modes, transparency groups), JPEG 2000, optional content, embedded files, file attachment and other post PDF 1.4 annotations, OpenType font programs and ICC profiles newer than version 2 are refused with the rest of the problems; subset CID fonts get a CIDSet stream read from the embedded program. - PDF/A-4, 4e and 4f output (`PdfA4`, `PdfA4E`, `PdfA4F`): PDF 2.0 with `pdfaid:rev` 2020. The information dictionary written holds only the modification date; the document keeps its values and the XMP carries them. Every embedded file is an associated file with its relationship; 4f needs at least one and takes any type, plain 4 and 4e take PDF/A-1, 2 and 4 files only, and 4e allows 3D and RichMedia annotations with their GoTo3DView and RichMediaExecute actions. - PDF/A checks for every part: actions are walked (open action, links, widgets, bookmarks and `Next` chains) and Launch, JavaScript, ResetForm, ImportData, Hide, media and other forbidden types, and named actions other than page navigation, are refused, as are additional actions on pages, widgets and fields, XFA, transfer functions, external stream data, PostScript and reference XObjects and OPI. LZW streams are re-encoded with Flate, image interpolation is switched off and NeedAppearances is removed. Saving an invoice as anything but PDF/A-3 is refused. - Signature validation, the X.509 checks of ETSI EN 319 102-1: certificate policy processing as in RFC 5280, 6.1 with policy mappings, policy constraints and inhibit anyPolicy, on the policy graph of RFC 9618 (`PdfValidationOptions.AcceptablePolicies`, `RequireExplicitPolicy`, `InhibitPolicyMapping`, `InhibitAnyPolicy`; `PdfCertificateValidation.Policy` and `ValidPolicies`); name constraints on DNS names, email addresses, URI hosts, directory names and IP addresses, the trust anchor's included (`PdfCertificateCheck.NameConstraints`), with hosts compared in one canonical form (one trailing dot dropped, IDN as ASCII, lower case, URIs read by `System.Uri`), directory strings decoded in every string type and prepared as RFC 4518 says, and anything that cannot be read or judged failing closed, including constraints that cannot be read, which fail every certificate below them; and the QCStatements of ETSI EN 319 412-5 (`PdfCertificateCheck.QcStatements`, `PdfQcStatements`), with the qualification they claim (`PdfCertificateValidation.Qualification`: QESig, QESeal, AdESig-QC and the rest). No trusted list is downloaded. Critical policy, name constraint and QC statement extensions no longer fail a certificate as unknown. - Text extraction is about 3.8 times faster with identical output: `ExtractText` on a 60 page, 27,000 word report in an embedded TrueType font went from 255 ms to 67 ms and from 300 MB to 69 MB allocated. Reading order sorts runs of touching letters instead of single letters and decides each line once per size and baseline; the content interpreter no longer copies the text state for every glyph, shares operator names and reads CID widths without a copy per glyph. - HTML conversion does less repeated work per line of text: a tab width is measured only for runs that contain a tab, the paragraph text is copied once for all its runs, and a font's OpenType features are read once per script. Output is unchanged. - The command line tool is published as native executables, compiled ahead of time, for linux-x64, linux-arm64, win-x64, osx-x64 and osx-arm64, attached to each GitHub release with a `SHA256SUMS.txt`. No .NET install is needed to run them. - `tools/PdfNetStack.Benchmarks` measures text extraction and HTML conversion with BenchmarkDotNet; it is not part of the solution or the packages. - Timing tests take the fastest of several runs, so a busy machine cannot fail them. - Shaping for more scripts, matching HarfBuzz glyph for glyph on the fonts we compare: - The Universal Shaping Engine, with its categories generated from the Unicode Character Database and Microsoft's overrides by `tools/make_shaping_data.py`: Balinese, Javanese, Sinhala, Tibetan, Sundanese, Batak, Buginese, Cham, Tai Tham, Lepcha, Meetei Mayek, Chakma, Grantha, Newa, Sharada, Tirhuta and the other Brahmic scripts HarfBuzz gives it, Indic fonts made for it (dev3 and the other version 3 tags), and Mongolian, N'Ko, Adlam and the other scripts that join like Arabic (a Mongolian free variation selector takes its letter's form), and Egyptian hieroglyph clusters. - Thai and Lao: sara am splits into nikhahit and sara aa, and fonts without Thai substitutions get their marks placed through the Windows or Macintosh private use glyphs. - Khmer (coeng ro and pre-base vowels reordered, split vowels decomposed, cfar), Myanmar (kinzi, medials and pre-base vowels reordered) and Hangul (syllables composed or decomposed for the font, ljmo, vjmo and tjmo, tone marks). - All shapers: invalid vowel sequences of the Indic and USE scripts get a dotted circle, cmap format 14 variation sequences pick their glyphs, a combining grapheme joiner that blocks no reordering is skipped by lookups, a ZWJ keeps a mark from attaching to the base before it (mark and mkmk see joiners), and contextual lookups whose nested lookups add glyphs index the sequence as HarfBuzz does. - Text extraction reads right to left lines in logical order when they start or end with a Latin word: the paragraph direction comes from both ends of the line, and punctuation next to the word stays in it ("To." rather than "To ."). Numbers in right to left lines follow the weak rules of UAX #9: after Arabic, "2026-114" (drawn 114-2026) reads in logical order, while dates, amounts and percentages stay whole. - Public benchmark corpus: `eng/corpus/manifest.json` lists 77 public domain and openly licensed PDFs (U.S. government publications, arXiv papers under CC BY 4.0, public domain scans and Chinese, Japanese and Korean documents from Wikimedia Commons, and test files of the veraPDF, qpdf, PDFium, pypdf and PDF Association projects) by URL, SHA-256, size, license, where the license is stated and what each file exercises. No file is committed. `tools/fetch_corpus.py` downloads and verifies them; `tools/corpus_bench.py` runs `tools/PdfNetStack.CorpusRunner` on each one, one process per measurement, for lossless and lossy optimization (size, time, peak memory), checks every rewrite again with `PdfEquivalence`, compares the text and decoded images the library reads before and after, compares PDFium renders, and writes a Markdown and a JSON report. The `Corpus benchmark` workflow runs it weekly and on demand. - Fixed, found by the corpus: - A cross-reference `/Prev` chain that loops is treated as damage and the file is rebuilt from its objects. A newest section naming itself as `/Prev` used to hide every older section, so the file opened without its page tree, and the optimizer (with `AllowRepairedFiles` or not) could rewrite it as a document without pages. - Content stream compaction judges whether a stream ends inside a comment from its tokens. A `%` inside a string on the last line, common in the two byte codes of composite fonts, was taken for a comment once the stream was compacted onto one line, so the proof rejected the rewrite and the whole document was returned unchanged. - JBIG2: the work budget is eight times the page instead of four. Scans that code the page as one symbol placed by a text region spend five, and failed to decode. - Optimizer, lossless structural passes, each proven by the equivalence check against its own analysis of the source (docs/FIDELITY.md gives the rules): - `PruneUnusedResources` removes fonts, XObjects, graphics states, patterns, shadings, color spaces and property lists that no content using the resource dictionary names, following forms, tiling patterns, Type 3 glyph procedures, soft masks and annotation appearances, and inherited and shared resource dictionaries. Anything it cannot read for certain is kept. - `CompactContentStreams` rewrites content streams with minimal white space, canonical numbers, shortest strings and no comments; inline image data is copied exactly. - `MergeFontSubsets` merges subsets of one TrueType font embedded separately, as merged documents carry them, into one program when no glyph any page shows changes. - Deduplication compares streams by their decoded content, so differently encoded copies of a font or image merge. - `Linearize` (opt-in) writes a linearized file (Annex F) with page offset, shared object and outline hint tables, laid out the way qpdf checks it. Inherited page attributes move onto the pages. `OptimizationStatistics` reports `ResourcesPruned`, `ContentStreamsCompacted`, `FontProgramsMerged` and `Linearized`; the command line tool takes `--linearize`, `--no-prune`, `--no-compact` and `--no-font-merge`. - `PdfDocument.OpenOnDemand(Stream)` opens a document on a seekable stream without reading it into memory: objects are parsed when first used and stream data is read when asked for, with 64 bit offsets. `PdfDocument.SaveIncremental(Stream)` saves the changes since opening as an incremental update, appending in place when given the source stream; existing signatures stay valid. - `PdfEquivalence.FindDifference` accepts the structural passes above and inherited page attributes moved onto pages. - `tools/make_optimizer_samples.py` and `tools/measure_optimizer.py` measure each pass on synthetic files and check linearized output with qpdf when it is installed. - Annotations: `PdfAnnotationCollection.AddStamp` (the fourteen standard stamps, or a custom one from a `PdfForm` or a drawing callback), `AddInk`, `AddSquare`, `AddCircle`, `AddLine` (with `PdfLineEnding` at either end), `AddPolygon`, `AddPolyline` and `AddFileAttachment` (the file embedded behind an icon; `PdfAnnotation.Attachment` reads it back). `PdfAnnotationOptions` sets the color, interior color, border width, dashes, opacity, comment, author and subject. Each comes with a generated appearance; below full opacity the drawing is a transparency group faded as a whole. PDF/A-3 output declares the files of file attachment annotations as associated files, and PDF/A-2 refuses them. - Annotation flattening: `PdfAnnotationCollection.Flatten(annotation)`, `Flatten(options)` for a page and `PdfDocument.FlattenAnnotations(options)` draw each annotation's normal appearance (or the one its state selects) into the page content, fitted to its rectangle by bounding box and matrix as ISO 32000-1, 12.5.5 describes, upright on rotated pages for NoRotate annotations and inside its optional content group, then remove it with its pop up. Hidden and NoView annotations stay; links and widgets stay unless `PdfFlattenOptions` asks for them, and a flattened widget leaves the form. `PdfAcroForm.Flatten` uses the same placement. - Form fields travel with their pages: `Pages.Import`, `Merge` and `Split` copy each widget's field and ancestors into the target form, with the source form's default appearance, alignment, resources and calculation order. A top level field whose name the target already uses is renamed with the first free suffix `_2`, `_3` and so on. Before, widgets were copied without their fields. - Format actions: a field whose format action calls `AFNumber_Format`, `AFPercent_Format`, `AFDate_FormatEx`, `AFDate_Format`, `AFTime_Format`, `AFTime_FormatEx` or `AFSpecial_Format` shows its value formatted in the generated appearance (red for negative numbers when asked), and `PdfTextField.FormattedText` returns it. The call is parsed and the formatting done in C#; no JavaScript runs, and any other script leaves the value as it is. - Fixed: form flattening threw on a widget whose appearance entry was not a stream. - HTML grid layout follows CSS Grid 1 and 2 further: `grid-auto-flow` with `column` and `dense` packing; line names in track lists and `repeat()`, placement by name and by `span name`, with the `name-start` and `name-end` lines of areas and the implicit lines past the explicit grid; implicit tracks before the explicit grid for negative lines; `subgrid` on rows, columns or both, with the parent's line names, the subgrid's own name list, its own gaps, and its items sizing the parent's tracks through its margins, borders and padding. `repeat(auto-fill)` counts the other tracks of the list when working out how many repetitions fit, and the empty tracks of `repeat(auto-fit)` collapse with their gaps. The `grid-template` and `grid` shorthands are read, area strings and `auto-flow` included. - HTML named pages: the `page` property works on any block in the block flow, not only children of `body` (CSS Paged Media 3, 8). A change of page name forces a break, and the content after it is laid out at its own page type's width. A box that spans the break is sliced: its top edge shows before the break and its bottom edge after, and the margin after the break is kept. - HTML pagination: a forced break before the first content or after the last no longer adds an empty page, so `break-before: page` on the first element starts the document on page one. - Fixed: HTML boxes with a `z-index` now paint in stacking order (CSS 2.1, Appendix E). A negative `z-index` paints over the stacking context's background and borders and under its blocks, floats and text; a positive one paints over all of it, in z-index order. A `position: fixed` box with a negative `z-index` paints under the content of every page, where before it painted over it. Positioned boxes, flex items and grid items with a `z-index`, and boxes with opacity below one or a transform, start a stacking context. - Fixed: HTML `text-decoration-style: wavy` draws a wave instead of a straight line, for underline, overline and line-through. Its amplitude and wavelength follow the line's thickness, and it is clipped to the decorated text. - PAdES B-T, B-LT and B-LTA. `PdfTimestampClient` builds RFC 3161 requests (hash, random nonce, optional policy) and checks the reply (status, hash, nonce, signature, a critical time stamping extended key usage); the application supplies the transport. `PdfSignatureOptions.Timestamp` adds a signature timestamp, `PdfSigner.AddDocumentTimestamp` a /DocTimeStamp, and `PdfSigner.AddValidationData` the document security store: certificate chains, OCSP responses and CRLs from an `IPdfRevocationSource`, and a VRI entry per signature, in an incremental update. Only revocation data that matches the certificate and is signed by its issuer or delegated responder is kept; delta and partitioned CRLs are not used. The library makes no network calls. - Signature validation: `PdfSigner.Verify(pdf, PdfValidationOptions)` builds chains to caller supplied trust anchors and checks validity at the timestamp's time when its authority validates (otherwise now), key usage (signer, CA with path length, time stamp authority) and revocation from the document security store or supplied data, with OCSP freshness and delegated responders. `PdfCertificateValidation` reports each check per certificate as passed, failed, indeterminate or not checked, with the reasons. `PdfSignatureInfo` now also reports document timestamps and signature timestamps. - Factur-X and ZUGFeRD: `PdfInvoice.Attach` embeds the invoice XML (checked for well formedness, without DTDs) as factur-x.xml, or xrechnung.xml for XRECHNUNG, with the profile's relationship; saving as PDF/A-3 writes the fx: XMP properties (MINIMUM to EXTENDED and XRECHNUNG) with their extension schema, checks the attachment is still there, and keeps the metadata when an opened invoice is saved again. - PDF/A-3: the catalog's /AF array now references the attachment's file specification itself instead of a copy of it. - CI: a veraPDF job validates PDF/A-2b, 2u, 3b and 3u files made by the library, including a Factur-X invoice signed to PAdES B-LTA. - Lossy image options for the optimizer, all off by default: - `MaxImageDpi` (`--max-dpi`) downsamples images drawn above 1.5 times the target resolution, measured from the largest placement found on pages, in forms, tiling patterns, soft mask groups and annotation appearances. The filter is Lanczos with three lobes. Masks, images with a color key mask or a /Matte soft mask, bilevel, indexed, 16 bit, Lab, Separation and DeviceN images, and images reachable from anything the measurement does not walk (Type 3 glyphs, thumbnails, alternates) are left alone. A downsampled JPEG stays a JPEG, never finer than it was. - `ConvertImagesToJpeg` (`--to-jpeg`) re-encodes raw 8 bit gray and RGB photographs as baseline JPEG at `JpegQuality` (forward DCT, libjpeg quality tables, 4:2:0 below quality 90, Huffman tables optimized per image). A classifier leaves line art, screenshots, charts, palette and bilevel images lossless. A lossy result is kept only when it is at least 10% smaller than the best lossless one. - `MinImageSsim` (`--min-ssim`) is a perceptual gate on luminance SSIM: any image whose lossy change, requantization included, falls below it stays as it was, and `ImagesKeptByQualityGate` counts them. The proof checks resampled and converted images against the source resampled to their new size (components, aspect, mean color, a coarse comparison and SSIM), and with a gate also requantized ones. An image whose data is carried over unchanged must keep its size. `PdfEquivalence.FindDifference` takes the same gate and the optimizer's exclusions: soft masks, masks, stencil and color keyed images must match exactly, and images with a /Mask or a /Matte soft mask keep their size. - A JPEG with a color key mask is no longer requantized, since the key matches exact sample values. - `ProgressiveJpeg` (`--progressive`) also writes every JPEG the optimizer produces as progressive, with libjpeg's scan script and Huffman tables per scan, and keeps it when smaller. Coefficients are unchanged, so this one is lossless. - `OptimizationStatistics` reports `ImagesDownsampled`, `ImagesConvertedToJpeg` and `ImagesKeptByQualityGate`, and the result's reason names every lossy change. - `CompressionEffort.Exhaustive` (`--exhaustive`): an original Deflate encoder in the manner of Zopfli, written from RFC 1951 and published descriptions: optimal parsing over every match length with costs refined over several passes, block splitting, and code lengths limited by package-merge. zlib's output is kept whenever it is smaller. On the synthetic files of `tools/make_lossy_corpus.py` it saves 3 to 5% more than level 9 on text, scans and photographs (nothing on line art and screenshots), for 2 to 150 times the CPU time, the most on long repetitive streams. - `PdfNetStack.Azure` is discontinued so the project can focus on PDF itself. Reading from and writing to storage belongs to the application, around the stream APIs. Version 1.0.0 stays on NuGet and keeps working. - Variable fonts: `PdfOpenTypeFont.GetInstance` gives a named instance (`NamedInstances`) or any axis values (`VariationAxes`, `IsVariable`), embedded as a static font. TrueType outlines move by their gvar deltas, with untouched points interpolated and composite offsets varied; advances come from HVAR or the phantom points, font metrics from MVAR (with hhea kept in step), control values from cvar, weight and width classes and the italic angle from the axes, through avar. CFF2 fonts load (at their default instance) and convert to CID-keyed CFF: blends resolve in charstrings and Private DICTs, subroutines are inlined, and argument runs longer than CFF's 48 operands are split. The shaper applies GDEF variation deltas to GPOS values and anchors, and GSUB and GPOS feature variations. Outlines, advances and metrics match fontTools' instancer and shaping matches HarfBuzz on the synthetic fixtures and on the variable fonts we compare. Damaged variation data is a `FontFormatException`. - Unicode normalization in the shaper, as HarfBuzz's normalizer does it: characters the font lacks decompose into ones it has, marks sort into canonical order (with the shaping tailorings for Hebrew, Arabic and others), a base and mark compose when the font has the composite, and spaces and the non-breaking hyphen without glyphs draw with the space and hyphen at their own widths. Font fallback, `CanRender` and HTML text use the same coverage. Data generated from the Unicode Character Database 16.0 by `tools/make_shaping_data.py`. - Indic shaping for Devanagari, Bengali, Gurmukhi, Gujarati, Oriya, Tamil, Telugu, Kannada and Malayalam, per Microsoft's script development specifications: syllables, base consonant, reph, pre-base matras and consonants, the basic forms one feature at a time, final reordering, presentation forms, old (deva) and new (dev2) font specifications, and dotted circles for broken clusters. It matches HarfBuzz on a synthetic Devanagari and Bengali fixture and on the three faces of a font covering all nine scripts. - Kashida justification (`PdfParagraph.Kashida`, off by default): justified Arabic lines elongate one join per word with tatweel, at the point Arabic typography prefers, up to three tatweel widths, before spaces widen. Tatweels carry no text, so copying gives the original words. - Fixed: a mark drawn with a vertical offset (Arabic dots and vowels in many fonts) moved every glyph after it by its horizontal offset, so letters failed to join and lines came out wider than measured. Spaces at the edge of a right to left run are now written outside its ActualText, so viewers no longer read them on the wrong side of the neighboring run. - Tagged PDF from HTML (`HtmlConverterOptions.Tagged`, on by default): headings, paragraphs, lists, tables with header and data cells, figures with their alt text, links tied to their annotations, sections and quotes form a structure tree in reading order (ISO 32000-1, 14.8); backgrounds, borders, text shadows, repeated table headers, fixed boxes and page margins are artifacts. The catalog is marked and viewers show the document title. - JPEG 2000 images decode (`PdfPageImage.Decode`, JPXDecode): JP2 files and raw codestreams, ITU-T T.800 part 1. Every progression order and progression change, precincts, layers, tiles and tile-parts, PPM, PPT, SOP and EPH, all code-block styles, ROI, the 5/3 and 9/7 wavelets, RCT and ICT, subsampled, signed and 1 to 16 bit components, JP2 palettes and channel definitions. The image dictionary's color space wins, otherwise the JP2 header's (ISO 32000-1, 7.4.9). Lossless images decode bit exact and lossy ones within one step of OpenJPEG. Sizes, counts and work are bounded. SMaskInData is not applied; HTJ2K (part 15) and part 2 extensions are not supported. - JBIG2 images decode (`PdfPageImage.Decode`, JBIG2Decode with JBIG2Globals): generic, refinement, text and halftone regions with their symbol and pattern dictionaries and code tables, arithmetic, Huffman and MMR coded, per ITU-T T.88. Output is bounded by the image's size, and damaged data is a `PdfFormatException`. Tested bit for bit against PDFium on the T.88 Annex H test datastream and synthetic streams. - Named destinations: `PdfDocument.NamedDestinations` lists the names of the /Dests name tree and the catalog's /Dests, resolves them to pages, and renames them with every bookmark, link and GoTo action that refers to them, in their own string or name form, rewriting the name tree as one sorted leaf; colliding renames are refused. `PdfOutline.DestinationName` gives the name a bookmark points at. - GIF images: `PdfImage.Load` reads GIF 87a and 89a (the first frame, interlaced or not, with its transparent color as a soft mask), so `DrawImage` and `` in HTML take them too. - BMP, TIFF and WebP images: `PdfImage.Load` reads BMP (1 to 32 bits, RLE4 and RLE8, bit fields, top down rows, alpha, an embedded JPEG or PNG), 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) and WebP (lossy and lossless, with alpha, the first frame of an animation). Alpha becomes a soft mask. A TIFF page stored as one strip of fax or JPEG data is embedded as is (CCITTFaxDecode, DCTDecode). `PdfImage.GetPageCount` and `PdfImage.Load(path, page)` give every page of a multi-page TIFF. Lossy WebP decodes to the pixels libwebp gives, bit for bit. Decoding is bounded, and damaged files fail with a `FormatException`. `` in HTML takes all three formats. - `PdfGraphics.DrawImage` fits an image into a box (`PdfImageFit`: fill, contain, cover, none, scale down), aligns it there (`PdfImageAlignment`, clipping what spills over) and fades it with an opacity. - Form creation: `PdfAcroForm.AddTextField`, `AddCheckBox`, `AddRadioGroup` (buttons may span pages), `AddComboBox`, `AddListBox`, `AddSignatureField` and `AddPushButton` (optionally opening a web address), styled with `PdfFieldOptions` (standard font, size, colors, border, tooltip, read only, required, alignment, multiline, password, length limit, comb, editable, multiple selection). Every new field comes with its appearance and the print flag. - New package `PdfNetStack.Ocr`: `PdfOcr.MakeSearchable` finds each page's scan, turns it upright, reads it and lays the words over it as invisible text; `Recognize` reports without changing the document. `OnnxOcrEngine` runs the PP-OCRv5 mobile models (PaddleOCR, Apache-2.0) on ONNX Runtime for Latin and Arabic script, with DB box detection, line orientation, CTC decoding, word boxes and right to left ordering in managed code, and model files checked against pinned SHA-256 hashes (`tools/fetch_ocr_models.py`). ONNX Runtime's telemetry is turned off before the first model loads. `IOcrEngine` plugs in any other engine. - New package `PdfNetStack.Ocr.Models` (Apache-2.0 models, versioned on its own from 1.0.0): the four PP-OCRv5 model files with their license and notices, copied next to the application on build and publish; `OnnxOcrEngine.Load()` without a folder loads them from there. The files are never committed: CI and the release workflow fetch them, and packing fails when one is missing or does not match its pin. The license check now also covers bundled files and keeps ONNX Runtime out of every package but `PdfNetStack.Ocr`. - OCR reads scans stored as several images: strips or tiles side by side, or a text mask over a background. The pieces are painted in drawing order into one gray canvas in page space, at the finest piece resolution up to `MaxOcrDpi`, decoded one at a time, with image count, combined pixels, canvas size and overlap capped before anything is decoded; word boxes map back to the page through the canvas. - Text extraction: `PdfPage.ExtractText()` returns a page's text in reading order, `GetLetters()` each glyph with its text, bounds, baseline, font name, visible size and render mode, and `GetWords()` the words with their bounds. Glyphs are placed as the specification positions them (character, word and horizontal scaling, rise, TJ adjustments, forms, Type 3 font matrices, vertical writing). Text comes from ToUnicode maps (bfchar and bfrange, ligatures, surrogate pairs, codes of 1 to 4 bytes), from the simple font encodings and /Differences with the Adobe Glyph List, from embedded CMaps with usecmap, the Identity and Unicode predefined CMaps, and embedded TrueType cmaps; spans marked with ActualText are replaced by their text, so right to left text the library writes reads back in logical order. Lines follow each run's own baseline, so rotated and vertical text read in order; columns of prose are found by XY cuts and read one after the other, right to left for Arabic and Hebrew pages, while tables keep reading row by row. CMaps, fonts and content are bounded and never throw. - CJK text extraction: Adobe's predefined CMaps for the Adobe-Japan1, GB1, CNS1, Korea1 and KR collections (all 180 of cmap-resources, among them every CJK CMap of ISO 32000-1 Table 118, such as 90ms-RKSJ-H, GBK-EUC-H, B5pc-H, KSCms-UHC-H and the Uni...-UCS2, UTF16, UTF8 and UTF32 forms) split codes and give CIDs, so widths and vertical metrics follow the CMap, and the CID to Unicode tables of mapping-resources-pdf give text to composite fonts without a ToUnicode map, by the CMap's collection or the CIDFont's /CIDSystemInfo (ISO 32000-1, 9.10.2). The data is packed into one Brotli stream per collection (196 KB in all) and decompressed on first use. Embedded CMaps that use a predefined one inherit its collection, and keep their own /WMode. - Simple fonts without /Encoding read their codes through the built-in encoding of an embedded CFF program (FontFile3, Type1C or OpenType): the Standard and Expert encodings and custom encodings of both formats with supplements, glyphs named through predefined or custom charsets. - `PdfGraphics.DrawInvisibleText`: invisible, searchable text stretched to a box, optionally rotated, for OCR text layers. A built in glyphless font (Type0, Identity-H, one empty glyph, identity ToUnicode) takes any script; right to left text and characters outside the BMP also carry ActualText. - Image extraction: `PdfPage.GetImages()` lists the images a page draws (XObjects, inside forms too, and inline images) with their placement, bounds and resolution. `PdfPageImage.Decode()` returns a `PdfBitmap` of 8 bit gray or RGB pixels from raw samples of 1 to 16 bits, from JPEGs (inverse DCT, fancy upsampling and color conversion as libjpeg does them, within two steps per sample) and from CCITT fax data (Group 3 one and two dimensional, Group 4, bit exact with libtiff), through the device, Cal, ICC based, Indexed and Lab spaces, and Separation and DeviceN (exact for a Type 2 tint transform, gray otherwise); `PdfBitmap.ToPng()` saves it. `GetEncodedData()` returns a JPEG or JPEG 2000 image as stored. - New package `PdfNetStack.Html`: HTML and CSS to PDF in pure C#, without a browser. `HtmlConverter.ConvertToPdf`, `ConvertToDocument` and `ConvertFileToDocument`, configured with `HtmlConverterOptions` (base address, resource loader, fonts, page style, extra style sheet, bookmarks). - HTML parsed the way browsers repair it, with the legacy presentational attributes. - CSS: cascade, inheritance, `var()`, Selectors Level 4, media queries for print, `@import`, `@font-face`, `@page` with margin boxes, `calc()` and friends, CSS Color 4. - Layout: blocks with margin collapsing, inline text with Unicode line breaking, bidi and shaping, floats, relative, absolute and fixed positioning, tables in both border models, flexbox, grid with fr tracks, `minmax()`, `repeat()` and named areas, inline blocks, lists, counters and generated content. - Paged media: page sizes and margins, `:first`, `:left` and `:right`, named pages laid out at their own size, running headers and footers with page counters and named strings (`string-set`, `string()`), running elements (`position: running()` shown with `content: element()`, any content: logos, tables, styled text, changing with the page where they stand, with page counters inside), margin boxes that widen when their neighbours are empty, forced and avoided breaks, orphans and widows, table headers repeated on every page. - Painting: backgrounds with images and gradients, rounded borders, shadows, outlines, transforms, `object-fit`, decorations; links and bookmarks; document metadata from the head. - Safe by default for untrusted HTML: no network access without a loader, files only from the base folder, and limits on nesting, selectors, `var()` expansion, spans and tiling. - It depends on the exact `PdfNetStack` version it ships with. - The `PdfNetStack` and `PdfNetStack.Azure` packages are checked against 1.0.0 on every pack, so a change that would break code built against the published version fails the build. - SVG: `PdfSvg.Load` and `Parse`, and `PdfGraphics.DrawSvg`, draw SVG as PDF vectors: shapes, paths with arcs, groups, `use` and `symbol`, nested viewports with `viewBox` and `preserveAspectRatio`, transforms, solid colors, linear and radial gradients, clip paths, opacity, dashes, text in the standard fonts, embedded images, and `