# 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 Call `PdfOptimizer.OptimizeFileAsync` with an input and an output path. The optimizer rewrites the file with compressed object streams and a compressed cross-reference stream, drops unreachable objects and unused resources, rewrites content streams compactly, recompresses streams, re-encodes JPEGs losslessly and merges duplicates. It then reads its own output back and compares it to the source object by object. When anything differs, or the saving is under 1%, the output is your original bytes. ```csharp title="compress.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("manual.pdf", "manual.optimized.pdf"); Console.WriteLine($"{result.Disposition}: {result.OriginalBytes:N0} to {result.OutputBytes:N0} bytes, saved {result.SavingPercent:F1}%"); Console.WriteLine(result.Reason); ``` ```bash dotnet add package PdfNetStack ``` The same job on the command line is `pdfnetstack optimize in.pdf out.pdf`, see [Command line](https://www.jadev-corp.com/cli/). More: https://www.jadev-corp.com/pdfnetstack/guides/compress-pdf/ --- # Edit PDF pages in C# and .NET `PdfDocument.Open` reads a file, `doc.Pages` is an ordered list you can change, and `doc.Save` writes a new single revision PDF. `Pages.Import` copies pages from any other document, with shared fonts and images copied once. ```csharp PdfDocument doc = PdfDocument.Open("contract.pdf"); doc.Pages.RemoveAt(0); // drop the cover doc.Pages.Move(2, 0); // the third page becomes the first doc.Pages[0].Rotate(90); // clockwise, in steps of 90 degrees doc.Pages.Add(PdfRectangle.A4); // a blank page at the end doc.Pages.Insert(1, PdfRectangle.Letter); // or anywhere PdfDocument annex = PdfDocument.Open("annex.pdf"); doc.Pages.Import(annex.Pages); // shared fonts and images are copied once doc.Save("contract-edited.pdf"); // optimized and verified by default ``` ```bash dotnet add package PdfNetStack ``` The same edits on the command line are `pdfnetstack merge`, `split`, `extract`, `rotate` and `meta`, see [Command line](https://www.jadev-corp.com/cli/). More: https://www.jadev-corp.com/pdfnetstack/guides/edit-pages/ --- # Work with large PDF files in C# Open the file with `PdfDocument.OpenOnDemand` on a seekable stream. Only the cross-reference data and the page tree are read; a page's content, fonts and images are read when you use them. Save with `SaveIncremental`, which appends the changed objects as an incremental update. Given the stream the document was opened on, it writes only the update, at the end of the file. ```csharp long before = new FileInfo("archive.pdf").Length; using (var stream = new FileStream("archive.pdf", FileMode.Open, FileAccess.ReadWrite)) { using PdfDocument doc = PdfDocument.OpenOnDemand(stream); // reads the xref and the page tree Console.WriteLine($"{doc.Pages.Count} pages"); string text = doc.Pages[3].ExtractText(); // reads page 4's content and fonts only Console.WriteLine(text.Split('\n')[0]); doc.Info.Title = "Operating manual, archived"; doc.SaveIncremental(stream); // appends the changes to the same file } long after = new FileInfo("archive.pdf").Length; Console.WriteLine($"{before:N0} bytes, then {after:N0}: {after - before:N0} bytes appended"); ``` On the sample manual, changing the title appended about 200 bytes to the file. More: https://www.jadev-corp.com/pdfnetstack/guides/large-files/ --- # Draw on PDF pages in C# and .NET Call `CreateGraphics()` on a page to get a `PdfGraphics` canvas. Coordinates are points (1/72 inch) from the top left corner of the page, with y pointing down. Dispose the canvas, or save the document, and the drawing is written into the page. ```csharp title="create-first.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; using PdfNetStack.Fonts; PdfDocument doc = PdfDocument.Create(); PdfPage page = doc.Pages.Add(PdfRectangle.A4); using (PdfGraphics g = page.CreateGraphics()) // origin top left, y down, points { g.DrawRectangle(null, PdfColor.FromHex("#1E3A8A"), 0, 0, page.Width, 120); g.DrawString("Hello, PDF", PdfStandardFont.HelveticaBold, 32, PdfColor.White, 40, 75); g.DrawString("Made with PdfNetStack.", PdfStandardFont.Helvetica, 12, PdfColor.Black, 40, 160); } doc.Save("hello.pdf"); ``` ![A page with a blue header band and the words Hello, PDF](https://www.jadev-corp.com/previews/hello.webp) More: https://www.jadev-corp.com/pdfnetstack/guides/drawing/ --- # Fonts and text in PDF with C# and .NET `DrawString` writes one line of text at a baseline. `PdfStandardFont` gives the 14 fonts every PDF viewer has, with nothing to embed. `PdfOpenTypeFont.Load` reads a .ttf, .otf, .ttc, .woff or .woff2 file and embeds only the glyphs you draw. ```csharp // The 14 standard fonts need no embedding: every PDF viewer has them. PdfFont bold = PdfStandardFont.HelveticaBold; g.DrawString("Invoice 2026-114", bold, 24, PdfColor.Black, 40, 60); // x, then the baseline y // Measure to align: right edge at 555 points. double width = bold.MeasureString("Total: €1,250.00", 14); g.DrawString("Total: €1,250.00", bold, 14, PdfColor.Black, 555 - width, 60); ``` ![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) More: https://www.jadev-corp.com/pdfnetstack/guides/fonts-and-text/ --- # Add images to PDF in C# and .NET Load an image with `PdfImage.Load` or an SVG drawing with `PdfSvg.Load`, then place it with `DrawImage` or `DrawSvg` on a page's canvas. Positions and sizes are points from the top left of the page. Each image is written once per document, however often it is drawn. ```csharp PdfImage photo = PdfImage.Load("photo.jpg"); // JPEG copied as is, EXIF orientation honored PdfImage logo = PdfImage.Load("logo.png"); // PNG alpha becomes a soft mask PdfSvg badge = PdfSvg.Load("badge.svg"); // SVG stays vector: sharp at any zoom g.DrawImage(photo, 40, 40, 320, 200); // stretched to the rectangle, in points g.DrawImage(logo, 380, 40, 150, 50); g.DrawSvg(badge, 380, 110, 150, 60); // viewBox and preserveAspectRatio decide the fit g.DrawImage(logo, 40, 290); // natural size: pixels at their recorded resolution, else 96 dpi ``` ![A photo, a transparent PNG logo and an SVG badge placed on a page](https://www.jadev-corp.com/previews/images.webp) More: https://www.jadev-corp.com/pdfnetstack/guides/images/ --- # Fill and create PDF forms in C# and .NET `doc.Form` is the document's form. Look a field up by its name, cast it to its kind and set the value: the field's appearance is redrawn at once. `Flatten` turns the values into plain page content. ```csharp ((PdfTextField)doc.Form["name"]!).Text = "Jamal Abdelkhalek"; ((PdfCheckBoxField)doc.Form["subscribe"]!).Checked = true; ((PdfRadioButtonField)doc.Form["plan"]!).Selected = "pro"; ((PdfChoiceField)doc.Form["country"]!).Select("Morocco"); doc.Save("application-filled.pdf"); // still a form: values can be changed in a viewer ``` More: https://www.jadev-corp.com/pdfnetstack/guides/forms/ --- # PDF links and annotations in C# and .NET `page.Annotations` lists a page's annotations and adds new ones. Positions use the same coordinates as the drawing canvas: points from the top left corner of the page, y down. Each new annotation comes with an appearance stream, so every viewer shows it the same way. ```csharp PdfDocument doc = PdfDocument.Open("manual.pdf"); PdfPage page = doc.Pages[0]; // Same coordinates as drawing: points from the top left of the page, y down. page.Annotations.AddLink(72, 95, 220, 20, new Uri("https://example.com/manual")); // opens a web address page.Annotations.AddLink(72, 160, 400, 14, doc.Pages[3], top: 100); // goes to page 4 page.Annotations.AddTextMarkup(PdfTextMarkup.Highlight, 70, 178, 410, 16, comment: "Check this line"); page.Annotations.AddTextMarkup(PdfTextMarkup.StrikeOut, 70, 196, 410, 16); page.Annotations.AddNote(500, 150, "Please review section 1.2", author: "Reviewer"); page.Annotations.AddFreeText(330, 230, 200, 40, "Updated for version 2.", new PdfTextStyle(PdfStandardFont.Helvetica, 11), border: PdfColor.FromHex("#2563EB"), fill: PdfColor.FromHex("#EFF6FF")); ``` ![A manual page with a highlighted line, a struck out line, a note icon and a free text box](https://www.jadev-corp.com/previews/annotations.webp) More: https://www.jadev-corp.com/pdfnetstack/guides/annotations/ --- # Digitally sign PDF files in C# and .NET Call `PdfSigner.Sign` with the PDF bytes and an `X509Certificate2` that has a private key. It appends a PAdES B-B signature in an incremental update and returns the signed file. `PdfSigner.Verify` lists every signature with what held and what did not. ```csharp byte[] signed = PdfSigner.Sign(File.ReadAllBytes("contract.pdf"), cert, new PdfSignatureOptions { Reason = "Approved", Location = "Brussels", // A visible box on page 1: x and y from the top left of the page, in points. Appearance = new PdfSignatureAppearance(PageIndex: 0, X: 340, Y: 30, Width: 210, Height: 60), }); File.WriteAllBytes("contract-signed.pdf", signed); ``` The certificate can come from a .pfx file, the Windows certificate store, or a smart card through its provider: anything `X509Certificate2` can reach with a private key. The sample creates a throwaway self-signed certificate so it runs anywhere. | Level | What it adds | How | |---|---|---| | B-B | The signature, with the signing certificate | `PdfSigner.Sign` | | B-T | A trusted time: an RFC 3161 timestamp on the signature | `PdfSignatureOptions.Timestamp` | | B-LT | Certificates, OCSP responses and CRLs in the document security store | `PdfSigner.AddValidationData` | | B-LTA | A document timestamp over the whole file, including that data | `PdfSigner.AddDocumentTimestamp` | ![A contract page with a visible signature box in the top right corner](https://www.jadev-corp.com/previews/signatures-visible.webp) More: https://www.jadev-corp.com/pdfnetstack/guides/signatures/ --- # Create PDF/A files in C# and .NET Set `Conformance` in `PdfSaveOptions` when you save. PdfNetStack first checks what it cannot repair and lists every problem in one exception. When the document passes, it adds an sRGB output intent, XMP metadata that matches the document information, and the entries PDF/A requires for attachments, then writes the file. ```csharp // PDF/A embeds every font: draw with an OpenType font, not the standard 14. PdfFont regular = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2"); PdfFont bold = PdfOpenTypeFont.Load("fonts/Inter-Bold.woff2"); PdfDocument doc = PdfDocument.Create(); doc.Info.Title = "Invoice 2026-114"; doc.Info.Author = "Accounts"; doc.Language = "en"; PdfPage page = doc.Pages.Add(PdfRectangle.A4); using (PdfGraphics g = page.CreateGraphics()) { g.DrawString("Invoice 2026-114", bold, 24, PdfColor.Black, 56, 90); g.DrawString("Total due: 1,250.00 EUR", regular, 12, PdfColor.Black, 56, 130); } // factur-x.xml, related as Alternative for the BASIC profile, described in the XMP on save. PdfInvoice.Attach(doc, xml, PdfInvoiceProfile.Basic); doc.Save("invoice-pdfa3.pdf", new PdfSaveOptions { Conformance = PdfConformance.PdfA3B }); ``` ![An invoice saved as PDF/A-3b with an embedded font](https://www.jadev-corp.com/previews/pdfa-invoice.webp) More: https://www.jadev-corp.com/pdfnetstack/guides/pdf-a/ --- # Encrypt and decrypt PDFs in C# and .NET Call `doc.Security.Encrypt` with a user password, an owner password and the permissions, then save. The file is written with AES-256 unless you ask for AES-128. Open a protected file by passing the password to `PdfDocument.Open`. ```csharp PdfDocument doc = PdfDocument.Open("report.pdf"); doc.Security.Encrypt( userPassword: "read", // asked for to open the file; "" lets anyone open it ownerPassword: "admin", // unlocks every permission permissions: PdfPermissions.Print | PdfPermissions.CopyContent); // AES-256 by default doc.Save("report-protected.pdf"); ``` ```bash dotnet add package PdfNetStack ``` The command line equivalents are `pdfnetstack encrypt` and `pdfnetstack decrypt`, see [Command line](https://www.jadev-corp.com/cli/#encrypt). More: https://www.jadev-corp.com/pdfnetstack/guides/encryption/ --- # Extract text from PDF files in C# Open the document and call `ExtractText()` on a page. Lines come top to bottom, words are separated by a space and lines by a line feed. `GetWords()` and `GetLetters()` return the same text with positions, fonts and sizes. ```csharp foreach (PdfPage page in doc.Pages) { Console.WriteLine(page.ExtractText()); // lines top to bottom, words spaced } ``` More: https://www.jadev-corp.com/pdfnetstack/guides/extract-text/ --- # Extract images from PDF files in C# `page.GetImages()` lists the images a page draws, in drawing order, with their placement. Save a JPEG as stored with `GetEncodedData()`, or call `Decode()` to get 8 bit gray or RGB pixels and save them with `SavePng`. ```csharp int n = 0; foreach (PdfPageImage image in images) { n++; if (image.Encoding == "DCTDecode") { File.WriteAllBytes($"image-{n}.jpg", image.GetEncodedData()); // the JPEG as stored } else if (image.Decode() is PdfBitmap bitmap) { bitmap.SavePng($"image-{n}.png"); // 8 bit gray or RGB pixels Console.WriteLine($"image-{n}.png: {bitmap.Format}, {bitmap.Width}x{bitmap.Height}"); } } ``` More: https://www.jadev-corp.com/pdfnetstack/guides/extract-images/ --- # Make scanned PDFs searchable with OCR in C# Load the recognizer with `OnnxOcrEngine.Load`, then call `MakeSearchable` on a document. For each page that is a scan, PdfNetStack finds the image, reads it, and lays every word over it as invisible text. Save the document as usual. ```csharp // Models: python tools/fetch_ocr_models.py models using var engine = OnnxOcrEngine.Load("models", new OnnxOcrEngineOptions { Languages = ["latin", "arabic"] }); var ocr = new PdfOcr(engine, new PdfOcrOptions { Languages = ["latin", "arabic"] }); PdfDocument doc = PdfDocument.Open("scan.pdf"); PdfOcrReport report = ocr.MakeSearchable(doc); doc.Save("scan-searchable.pdf"); // looks the same, now selects, copies and searches ``` More: https://www.jadev-corp.com/pdfnetstack/guides/ocr/ --- # HTML to PDF in C# without a browser Call `HtmlConverter.ConvertToPdf` with an HTML string to get the PDF bytes. The markup is parsed the way browsers repair it, styled with the CSS cascade, laid out in boxes and split into pages by the paged media rules. ```csharp byte[] pdf = HtmlConverter.ConvertToPdf("

Invoice 42

Total: 1,210.00 EUR

"); File.WriteAllBytes("invoice.pdf", pdf); ``` The package is `PdfNetStack.Html`. It depends on the `PdfNetStack` version it ships with, and runs on .NET 8 and later and on .NET Framework 4.6.2 and later through .NET Standard 2.0. More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/ --- # CSS support in PdfNetStack.Html PdfNetStack.Html implements the CSS a print template needs: the cascade, Selectors Level 4, block, inline, table, flex and grid layout, floats, positioning, paged media, web fonts, backgrounds, borders, shadows and transforms. It has no multi-column layout, no vertical writing modes and no JavaScript. ```csharp const string Css = """ :root { --accent: oklch(0.55 0.2 260); --gap: 10pt } body { font-family: sans-serif; font-size: 9.5pt; color: #0f172a } h1 { font-size: clamp(16pt, 4vw, 24pt); color: var(--accent); margin: 0 0 8pt } .page { display: grid; grid-template-columns: 140pt 1fr; grid-template-areas: "side main" "foot foot"; gap: var(--gap) } aside { grid-area: side; padding: 8pt; border-radius: 6pt; background: radial-gradient(circle at top left, #dbeafe, #ffffff) } main { grid-area: main } footer { grid-area: foot; border-top: 1pt dashed #94a3b8; padding-top: 4pt; color: #64748b } .cards { display: flex; flex-wrap: wrap; gap: var(--gap) } .card { flex: 1 1 120pt; padding: 8pt; border: 1pt solid #cbd5e1; border-radius: 6pt; box-shadow: 0 2pt 4pt rgb(15 23 42 / 0.15) } .card:nth-child(2 of .card) { border: 3pt double var(--accent) } .card :is(h3, h4) { margin: 0 0 4pt; text-transform: uppercase; letter-spacing: 0.05em } .figure { float: right; width: 90pt; margin: 0 0 6pt 8pt; position: relative } .badge { position: absolute; top: -6pt; right: -6pt; padding: 2pt 4pt; color: white; background: #dc2626; border-radius: 4pt; transform: rotate(8deg) } .note { width: calc(100% - 20pt); text-decoration: underline wavy #f59e0b } .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(60pt, 1fr)); grid-auto-flow: row dense; gap: 4pt; margin: 6pt 0 } .gallery div { background: #e0e7ff; padding: 4pt; border-radius: 3pt } .gallery .wide { grid-column: span 2; background: #c7d2fe } .specs { display: grid; grid-template-columns: [label] max-content [value] 1fr; column-gap: 8pt } .specs > div { grid-column: label / -1; display: grid; grid-template-columns: subgrid; border-bottom: 0.5pt solid #cbd5e1 } .specs b { grid-column: label } ol.steps { counter-reset: step; list-style: none; padding: 0 } ol.steps li { counter-increment: step; margin-bottom: 3pt } ol.steps li::before { content: "Step " counter(step) ": "; font-weight: bold; color: var(--accent) } li:not(:last-child) { color: #334155 } """; ``` ![A page laid out with a grid: a sidebar with a radial gradient, three cards in a wrapping flex row, a floated figure with a rotated badge, a grid gallery with a spanning item, a subgrid table and numbered steps](https://www.jadev-corp.com/previews/html-layout.webp) More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/css-support/ --- # Fonts, images and resources in HTML to PDF Convert a file with `ConvertFileToDocument`, or set `BaseUri` to the folder that holds the assets, and relative URLs in ``, ``, `url()` and `@font-face` resolve there. Fonts come from `@font-face`, then the `Fonts` collection you pass, then installed fonts; the generic families map to the standard PDF fonts. To load from the web, pass a `ResourceLoader` that decides per address. ```csharp string folder = Path.GetFullPath(".") + Path.DirectorySeparatorChar; var options = new HtmlConverterOptions { BaseUri = new Uri(folder), // fonts/, photo.jpg, logo.png and photo.webp resolve here UseSystemFonts = false, // only @font-face and the standard 14 fonts ResourceLoader = uri => { // With a loader, every address comes here: decide per address, null means "do not load". if (uri.IsFile) return Path.GetFullPath(uri.LocalPath).StartsWith(folder) ? File.ReadAllBytes(uri.LocalPath) : null; if (uri.Host != "assets.example.com") return null; return remote.TryGetValue(uri.AbsoluteUri, out string? css) ? Encoding.UTF8.GetBytes(css) : null; }, }; ``` ![A page converted from HTML: a photograph with rounded corners, a PNG logo, two SVG shapes, a label in Inter and a line of Arabic text](https://www.jadev-corp.com/previews/html-assets.webp) More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/fonts-and-assets/ --- # Page headers, footers and numbers in HTML Write an `@page` rule with a size, margins and margin boxes. `counter(page)` and `counter(pages)` give the page number and the page count, `string()` and `element()` carry text or whole elements from the document into the margins, and `break-before`, `break-after` and `break-inside` control where pages end. ```css @page { size: A4; margin: 22mm 18mm 24mm; @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 8pt } } ``` The `PageStyle` option sets a size and margins that apply before the document's own `@page` rules; its default is `size: A4; margin: 2cm`. More: https://www.jadev-corp.com/pdfnetstack/guides/html-to-pdf/paged-media/ --- # Low level PDF objects in C# `doc.Catalog` is the root of the object graph and `page.Dictionary` is a page's own dictionary. `doc.Resolve` follows references, `doc.AddObject` adds new indirect objects, and `doc.GetStreamData` decodes a stream. Changes are written by the next `Save`. ```csharp PdfDocument doc = PdfDocument.Open("report.pdf"); doc.Catalog.Set("PageMode", new PdfName("UseOutlines")); // open with the bookmarks panel shown PdfPage page = doc.Pages[0]; if (doc.Resolve(page.Dictionary["Resources"]) is PdfDictionary resources && doc.Resolve(resources["Font"]) is PdfDictionary fonts) { Console.WriteLine($"Fonts on page 1: {string.Join(", ", fonts.Keys)}"); } ``` More: https://www.jadev-corp.com/pdfnetstack/guides/low-level-objects/ --- # Add page numbers to a PDF in C# Open the document, call `CreateGraphics` on each page, and draw the label with `DrawString`. `MeasureString` gives the label's width so it can be centered. The canvas origin is the top left corner of the page, y grows downward, and units are points. ```csharp title="recipe-add-page-numbers.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; using PdfNetStack.Fonts; PdfDocument doc = PdfDocument.Open("report.pdf"); PdfFont font = PdfStandardFont.Helvetica; for (int i = 0; i < doc.Pages.Count; i++) { PdfPage page = doc.Pages[i]; string label = $"Page {i + 1} of {doc.Pages.Count}"; double width = font.MeasureString(label, 9); using PdfGraphics g = page.CreateGraphics(); // origin top left, y down, points g.DrawString(label, font, 9, PdfColor.Gray(0.35), (page.Width - width) / 2, page.Height - 30); } doc.Save("report-numbered.pdf"); ``` More: https://www.jadev-corp.com/pdfnetstack/recipes/add-page-numbers/ --- # Add a signature field to a PDF in C# Call `doc.Form.AddSignatureField` with the page, a field name and the box position and size, measured from the page's top left corner in points. `PdfFieldOptions` sets the border, background and tooltip. ```csharp title="recipe-add-signature-field.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; PdfDocument doc = PdfDocument.Open("contract.pdf"); PdfPage last = doc.Pages[^1]; doc.Form.AddSignatureField(last, "signature", 72, 730, 220, 60, new PdfFieldOptions { BorderColor = PdfColor.Gray(0.5), ToolTip = "Sign here", }); doc.Save("contract-to-sign.pdf"); Console.WriteLine(string.Join(", ", PdfDocument.Open("contract-to-sign.pdf").Form.Fields.Select(f => $"{f.Name} ({f.Kind})"))); ``` More: https://www.jadev-corp.com/pdfnetstack/recipes/add-signature-field/ --- # Add a watermark to a PDF in C# Draw on each page with `CreateGraphics`, rotate the canvas about the page center with `RotateAtTransform`, and draw the text with a color that has an alpha below 1. Positive angles turn clockwise, so -45 runs the text up from bottom left to top right. ```csharp title="recipe-add-watermark.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; using PdfNetStack.Fonts; PdfDocument doc = PdfDocument.Open("contract.pdf"); PdfFont font = PdfStandardFont.HelveticaBold; double size = 110; double width = font.MeasureString("DRAFT", size); foreach (PdfPage page in doc.Pages) { using PdfGraphics g = page.CreateGraphics(); g.RotateAtTransform(-45, page.Width / 2, page.Height / 2); // positive angles turn clockwise g.DrawString("DRAFT", font, size, PdfColor.FromHex("#DC2626").WithAlpha(0.25), (page.Width - width) / 2, page.Height / 2 + size * 0.35); } doc.Save("contract-draft.pdf"); ``` ![The first page of the contract with a red DRAFT watermark across it](https://www.jadev-corp.com/previews/recipe-watermark.webp) More: https://www.jadev-corp.com/pdfnetstack/recipes/add-watermark/ --- # Write Arabic text in a PDF in C# Load a font that covers Arabic with `PdfOpenTypeFont.Load`, put the text in a `PdfParagraph` with `Direction = PdfTextDirection.RightToLeft`, and draw it with `DrawText`. Letters join and take their contextual forms, and the text carries ActualText, so it copies and searches in logical order. ```csharp title="recipe-arabic-text-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; using PdfNetStack.Drawing; using PdfNetStack.Fonts; PdfFont arabic = PdfOpenTypeFont.Load("fonts/NotoSansArabic-Regular.woff2"); // embedded as a subset PdfFont latin = PdfOpenTypeFont.Load("fonts/Inter-Regular.woff2"); var style = new PdfTextStyle(arabic, 20) { Fallbacks = [latin] }; // digits and the euro sign PdfDocument doc = PdfDocument.Create(); PdfPage page = doc.Pages.Add(PdfRectangle.A4); using (PdfGraphics g = page.CreateGraphics()) { var paragraph = new PdfParagraph { Direction = PdfTextDirection.RightToLeft, LineHeight = 1.6 } .Add("المبلغ المستحق: 1,250.00 € خلال ثلاثين يوما من تاريخ الفاتورة.", style); g.DrawText(paragraph, 60, 60, page.Width - 120); // top left corner and width } doc.Save("arabic.pdf"); Console.WriteLine(PdfDocument.Open("arabic.pdf").Pages[0].ExtractText()); // logical order ``` ![An Arabic sentence drawn right to left at the top of a page](https://www.jadev-corp.com/previews/recipe-arabic.webp) More: https://www.jadev-corp.com/pdfnetstack/recipes/arabic-text-pdf/ --- # Batch compress PDFs from the command line Install the `pdfnetstack` tool and run `batch` with an input and an output folder. Each file is optimized losslessly and verified, and `--report` writes one CSV row per document. Without an output folder, `batch` only measures and writes nothing. ```bash dotnet tool install -g PdfNetStack.Tool pdfnetstack batch ./incoming ./optimized --report report.csv pdfnetstack batch ./incoming # measure only, writes nothing pdfnetstack batch ./incoming ./optimized --jpeg-quality 60 --jobs 4 ``` `--jobs` sets how many documents run in parallel, `--threads` the threads per document. The tool needs the .NET 8 or later runtime. More: https://www.jadev-corp.com/pdfnetstack/recipes/batch-compress-pdf-cli/ --- # Check two PDFs for equivalence in C# `PdfEquivalence.FindDifference` walks both object graphs from the trailer and compares every dictionary, array and reference, and every stream after decoding. It returns `null` when the two are equivalent, or a description of the first difference. ```csharp byte[] original = File.ReadAllBytes("report.pdf"); foreach (string candidate in new[] { "report-optimized.pdf", "report-edited.pdf" }) { string? difference = PdfEquivalence.FindDifference(original, File.ReadAllBytes(candidate)); Console.WriteLine($"{candidate}: {difference ?? "equivalent"}"); } ``` ```bash dotnet add package PdfNetStack ``` In the sample, the optimized copy is equivalent and the copy with a page removed is not: `trailer/Root/Pages/Kids: array length 3 vs 2`. More: https://www.jadev-corp.com/pdfnetstack/recipes/check-pdf-equivalence/ --- # Downsample images in a PDF in C# Set `MaxImageDpi` on `PdfOptimizerOptions`. The optimizer measures each image at the largest size it is drawn anywhere in the document, and resamples images drawn above 1.5 times the target down to the target, with a Lanczos filter. A raw image stays lossless after resampling and a JPEG stays a JPEG, so nothing changes format unless you also set `ConvertImagesToJpeg`. ```csharp var options = new PdfOptimizerOptions { MaxImageDpi = 150 }; PdfOptimizationResult result = await PdfOptimizer.OptimizeFileAsync("oversampled.pdf", "downsampled.pdf", options); Console.WriteLine($"{result.Statistics.ImagesDownsampled} image(s) downsampled, {result.SavingPercent:F1}% saved"); Console.WriteLine(result.Reason); ``` The sample's photograph, 1600 pixels drawn 3 inches wide (about 530 dpi), becomes a 150 dpi image and the file shrinks by 93.8%. More: https://www.jadev-corp.com/pdfnetstack/recipes/downsample-pdf-images/ --- # Extract all images from a PDF in C# `GetImages` lists the images a page draws, with their placement and resolution. A JPEG (`DCTDecode`) is written as stored with `GetEncodedData`, so nothing is recompressed. Other images are decoded to 8 bit gray or RGB pixels with `Decode` and saved as PNG. ```csharp title="recipe-extract-images-from-pdf.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument doc = PdfDocument.Open("manual.pdf"); for (int p = 0; p < doc.Pages.Count; p++) { IReadOnlyList images = doc.Pages[p].GetImages(); for (int i = 0; i < images.Count; i++) { PdfPageImage image = images[i]; string name = $"page{p + 1}-image{i + 1}"; if (image.Encoding == "DCTDecode") { File.WriteAllBytes(name + ".jpg", image.GetEncodedData()); // the JPEG as stored } else if (image.Decode() is PdfBitmap bitmap) { bitmap.SavePng(name + ".png"); // 8 bit gray or RGB pixels } Console.WriteLine($"{name}: {image.Width}x{image.Height}, {image.Encoding ?? "raw"}, {image.HorizontalResolution:0} dpi"); } } ``` More: https://www.jadev-corp.com/pdfnetstack/recipes/extract-images-from-pdf/ --- # Extract pages from a PDF in C# Create an empty document with `PdfDocument.Create`, then import the pages you want with `Pages.Import`. Page numbers in the sample start at 1. Fonts and images shared between the pages are copied once. ```csharp title="recipe-extract-pdf-pages.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument source = PdfDocument.Open("manual.pdf"); int[] keep = [1, 2, 5]; // page numbers, from 1, in the order you want them PdfDocument extract = PdfDocument.Create(); extract.Pages.Import(keep.Select(n => source.Pages[n - 1])); extract.Save("manual-extract.pdf"); Console.WriteLine($"manual-extract.pdf: {extract.Pages.Count} pages"); ``` ```bash dotnet add package PdfNetStack ``` More: https://www.jadev-corp.com/pdfnetstack/recipes/extract-pdf-pages/ --- # Fill and flatten a PDF form in C# Look fields up by name in `doc.Form`, cast to the field type, and set the value. Setting a value redraws the field's appearance, so the filled form looks the same in every viewer. `Flatten` stamps the appearances into the pages and removes the form, so the values can no longer be edited. ```csharp title="recipe-fill-and-flatten-pdf-form.cs" #:package PdfNetStack@1.2.0 using PdfNetStack; PdfDocument doc = PdfDocument.Open("application.pdf"); ((PdfTextField)doc.Form["name"]!).Text = "Jamal Abdelkhalek"; ((PdfCheckBoxField)doc.Form["subscribe"]!).Checked = true; ((PdfRadioButtonField)doc.Form["plan"]!).Selected = "pro"; ((PdfChoiceField)doc.Form["country"]!).Select("Morocco"); doc.Form.Flatten(); // values become page content, the form is removed doc.Save("application-filled.pdf"); ``` ![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) More: https://www.jadev-corp.com/pdfnetstack/recipes/fill-and-flatten-pdf-form/ --- # Flatten annotations in a PDF in C# `PdfDocument.FlattenAnnotations` draws the appearance of every markup annotation (notes, text markup, free text, stamps, ink, shapes and file attachments) into its page's content and removes the annotation with its pop up. Links and form fields stay unless you ask for them. ```csharp PdfDocument doc = PdfDocument.Open("contract-reviewed.pdf"); int flattened = doc.FlattenAnnotations(); // every page; links and form fields stay doc.Save("contract-flat.pdf"); Console.WriteLine($"{flattened} annotations are now page content"); ``` To flatten a single annotation, pass it to its page's collection: ```csharp PdfDocument other = PdfDocument.Open("contract-reviewed.pdf"); PdfPage page = other.Pages[0]; PdfAnnotation stamp = page.Annotations.First(a => a.Subtype == "Stamp"); page.Annotations.Flatten(stamp); // only the stamp; the rest stay editable other.Save("contract-stamped.pdf"); ``` More: https://www.jadev-corp.com/pdfnetstack/recipes/flatten-pdf-annotations/ --- # Convert an HTML invoice to PDF in C# Pass the HTML to `HtmlConverter.ConvertToPdf` from the `PdfNetStack.Html` package. The `@page` rule sets the paper size and margins, and its `@bottom-center` margin box numbers the pages. No browser runs, and nothing is fetched from the network. ```csharp title="recipe-html-invoice-to-pdf.cs" #:package PdfNetStack.Html@1.2.0 using PdfNetStack.Html; string html = """ Invoice 42

Invoice 42

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

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