Edit PDF pages in C# and .NET
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
Section titled “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.
PdfDocument doc = PdfDocument.Open("contract.pdf");
doc.Pages.RemoveAt(0); // drop the coverdoc.Pages.Move(2, 0); // the third page becomes the firstdoc.Pages[0].Rotate(90); // clockwise, in steps of 90 degreesdoc.Pages.Add(PdfRectangle.A4); // a blank page at the enddoc.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 defaultdotnet add package PdfNetStackThe same edits on the command line are pdfnetstack merge, split, extract, rotate and
meta, see Command line.
Open and create
Section titled “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.
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
Section titled “Merge, split and extract”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:
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:
PdfDocument source = PdfDocument.Open("manual.pdf");PdfDocument excerpt = PdfDocument.Create();excerpt.Pages.Import([source.Pages[4], source.Pages[0], source.Pages[1]]); // any orderexcerpt.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. A document
opened with only its user password gives up its pages only when its permissions allow copying.
Metadata
Section titled “Metadata”doc.Info holds the document information dictionary: title, author, subject, keywords, creator,
producer and the two dates, plus any custom entry by name.
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 tooforeach (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 output keeps both in sync.
Bookmarks
Section titled “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.
PdfOutline summary = doc.Outlines.Add("Summary", doc.Pages[0]);summary.Add("Revenue", doc.Pages[1]); // nested under Summarysummary.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
Section titled “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.
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<string, string>{ ["_TOC_d1e10"] = "introduction", ["_TOC_d1e20"] = "installation",});Console.WriteLine($"{changed} references renamed");doc.Save("guide-renamed.pdf");Save options
Section titled “Save options”Save writes a new single revision file. By default it runs the lossless
optimizer 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.
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 optionsusing (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 |
Limits
Section titled “Limits”- Saving in full with
Savewrites 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. To add a signature without breaking earlier ones, see Signatures. - A document opened with only the user password of an encrypted file can be read but not saved.
Openreads the whole file into memory.OpenOnDemand(1.1) reads objects as they are used, see Large files and Memory and performance.
Related
Section titled “Related”- Compress PDF: what the save time optimizer does
- Encryption: open protected files and protect your output
- Command line:
merge,split,extract,rotateandmeta - API: PdfDocument, PdfPageCollection, PdfPage, PdfDocumentInfo, PdfOutline, PdfNamedDestinations, PdfSaveOptions
Questions
- 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.