# 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<string, string>
{
    ["_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/
