Delete, extract, reorder, merge and split PDF pages
Build a new PDF from pages of PDFs you already have: in any order, with pages left out or repeated. Pages are moved whole. Nothing is rasterized or re-encoded, so each page looks exactly as it did and its text can still be selected.
These classes are in the DocWright.Formats.Pdf package, which the DocWright package already references. For a worked example that converts Word papers and then assembles them, see the tutorial Assemble a board pack.
Note
WordDocument.Merge and Split (Bookmarks, merge and split) combine Word documents before rendering. PdfPages works on PDFs that already exist, from any source.
The five operations
using DocWright.Core.Primitives;
using DocWright.Formats.Pdf;
// Delete pages 2 and 4 into a new file.
PdfPages.Delete("report.pdf", PageRange.Parse("2,4"), "report-trimmed.pdf");
// Keep pages 1-3 only.
PdfPages.Extract("report.pdf", PageRange.Parse("1-3"), "report-first-half.pdf");
// Reverse the order. A reorder names every page exactly once.
PdfPages.Reorder("report.pdf", [6, 5, 4, 3, 2, 1], "report-reversed.pdf");
// Merge whole files, in the order given.
PdfAssemblyResult merged = PdfPages.Merge(["cover.pdf", "report.pdf", "appendix.pdf"], "combined.pdf");
// Split into parts: one output file per range.
PdfPages.Split("combined.pdf",
[PageRange.Parse("1"), PageRange.Parse("2-7"), PageRange.Parse("8-")],
["part-cover.pdf", "part-report.pdf", "part-appendix.pdf"]);
// Change a file in place: written beside it, then swapped in only if the write succeeded.
File.Copy("report.pdf", "working-copy.pdf", overwrite: true);
PdfPages.DeleteInPlace("working-copy.pdf", PageRange.Parse("6"));
Output (all output from this page's samples):
report-trimmed.pdf 4 page(s)
report-first-half.pdf 3 page(s)
report-reversed.pdf 6 page(s)
combined.pdf 9 page(s)
part-cover.pdf 1 page(s)
part-report.pdf 6 page(s)
part-appendix.pdf 2 page(s)
working-copy.pdf 5 page(s)
extracted 1 page, 21398 bytes, to a stream
report has 6 pages
custom.pdf: 6 pages from 3 sources
"5,1" -> 1,5 (contains 3: False)
A reorder must name every page exactly once. The document has 6 page(s) and the order names 2. (Parameter 'order')
merge has losses: True, dropped: [Info], links lost: 0, bookmarks removed: 0
strict extract: 2 pages, losses: False
strict merge: Strict mode: The document-level entry /Info was not carried into the assembled output.
encryption removed: True
| Method | Does | In place |
|---|---|---|
Delete(source, pages, output) |
Writes every page except those in pages. |
DeleteInPlace |
Extract(source, pages, output) |
Writes only the pages in pages. |
ExtractInPlace |
Reorder(source, order, output) |
Writes the pages in the order given. Every page must appear exactly once. | ReorderInPlace |
Merge(sources, output) |
Writes all pages of each source, in order. | — |
Split(source, parts, outputs) |
Writes one file per range, and returns one result per file. | — |
Page numbers start at 1. The …InPlace methods write to a file beside the target and replace the target only after the write succeeds, so a failure never leaves you without either file.
Streams and byte arrays
using DocWright.Core.Primitives;
using DocWright.Formats.Pdf;
// Every operation also takes bytes in and writes to a stream: no temporary files in a web service.
byte[] upload = File.ReadAllBytes("report.pdf");
using var response = new MemoryStream();
PdfAssemblyResult firstPage = PdfPages.Extract(upload, PageRange.Single(1), response);
Console.WriteLine($"extracted {firstPage.PageCount} page, {firstPage.BytesWritten} bytes, to a stream");
Every operation has an overload that reads byte[] and writes to a Stream, which is what a web endpoint needs. PdfAssemblyResult.BytesWritten reports the output size.
The engine: any sequence of pages
All five operations are shortcuts for PdfPageAssembly. Use it directly to interleave pages from several sources, repeat a page, or give each source its own password:
using DocWright.Core.Primitives;
using DocWright.Formats.Pdf;
using (var assembly = new PdfPageAssembly())
{
int cover = assembly.AddSource("cover.pdf");
int report = assembly.AddSource("report.pdf");
int appendix = assembly.AddSource(File.ReadAllBytes("appendix.pdf")); // paths, bytes or streams
Console.WriteLine($"report has {assembly.GetSourcePageCount(report)} pages");
assembly
.AppendAll(cover) // the cover
.AppendRange(report, PageRange.Parse("1-2")) // report pages 1 and 2
.AppendPage(appendix, 1) // appendix page 1, interleaved
.AppendPage(report, 6) // the report's last page
.AppendPage(cover, 1); // the cover again, as a back page
PdfAssemblyResult result = assembly.Save("custom.pdf"); // or WriteTo(stream)
Console.WriteLine($"custom.pdf: {result.PageCount} pages from {result.SourceCount} sources");
}
1 · Cover
2 · Report p1
3 · Report p2
4 · Appendix p1
5 · Report p6
6 · CoverConvertPdfToImages. Download: custom.pdf.AddSourceaccepts a path, abyte[]or aStream, with an optional password. It returns the index you pass to theAppend…methods.- Appending the same page twice gives two output pages that share one copy of the content, so a repeated page adds almost nothing to the file size.
PdfPageAssemblykeeps its sources open until you dispose it. Use one instance per thread.
Page ranges are sets
using DocWright.Core.Primitives;
using DocWright.Formats.Pdf;
// A PageRange is a set of pages, not a sequence: "5,1" appends pages 1 and 5, in that order.
PageRange range = PageRange.Parse("5,1");
Console.WriteLine($"\"5,1\" -> {range} (contains 3: {range.Contains(3)})");
// For an exact sequence, use AppendPage (or Reorder).
using (var assembly = new PdfPageAssembly())
{
int report = assembly.AddSource("report.pdf");
foreach (int page in new[] { 5, 1 })
{
assembly.AppendPage(report, page);
}
assembly.Save("five-then-one.pdf");
}
// A reorder that doesn't name every page is refused, not guessed at.
try
{
PdfPages.Reorder("report.pdf", [3, 1], "never-written.pdf");
}
catch (ArgumentException error)
{
Console.WriteLine(error.Message);
}
PageRange.Parse accepts "3", "1-3", "1-3,7,9-" (page 9 to the end) and combinations of these. A range is a set, so "5,1" and "1,5" are the same range, and AppendRange always adds the pages in ascending order. When the order matters, call AppendPage for each page, or use Reorder.
Reorder refuses a list that doesn't include every page, because "reorder pages 3 and 1" could mean keep only those two or move those two to the front, and guessing wrong silently deletes pages. To keep a subset, use Extract. For any other sequence, including repeated pages, use AppendPage.
What is carried over, and what is reported
Links, named destinations, bookmarks (outlines) and page labels all point at pages, so they are rewritten to fit the new document:
- Links to a page that is still there are redirected to it. If that page was repeated, they go to its first copy. If the page was removed, the link action is removed but the annotation is kept, so the page still looks the same.
- Bookmarks are pruned and rebuilt. A bookmark is kept if its own target page survives, or if any bookmark nested under it does.
- Page labels are recalculated for the new page order, so a merge doesn't number itself "i, ii, 1, 2, i, ii".
- Inherited page settings (media box, crop box, rotation, resources) are copied onto each page, so an A3 page doesn't turn into US Letter when it is extracted.
Anything that can't be carried over is counted in PdfAssemblyResult, and each loss also raises a diagnostic (DXP8011–DXP8016):
using DocWright.Core;
using DocWright.Core.Primitives;
using DocWright.Formats.Pdf;
// Almost every merge reports a loss: two documents' /Info metadata can't be combined.
Console.WriteLine($"merge has losses: {merged.HasLosses}, dropped: [{string.Join(", ", merged.DroppedDocumentEntries)}], "
+ $"links lost: {merged.RemovedDestinationCount}, bookmarks removed: {merged.RemovedOutlineEntryCount}");
// StrictMode turns any loss into an exception. It suits single-source work...
var strict = new PdfAssemblyOptions { StrictMode = true };
PdfAssemblyResult chapter = PdfPages.Extract("report.pdf", PageRange.Parse("1-2"), "strict-extract.pdf", strict);
Console.WriteLine($"strict extract: {chapter.PageCount} pages, losses: {chapter.HasLosses}");
// ...and refuses nearly every merge, because of that /Info entry.
try
{
PdfPages.Merge(["cover.pdf", "report.pdf"], "strict-merge.pdf", strict);
}
catch (UnsupportedFeatureException error)
{
Console.WriteLine($"strict merge: {error.Message}");
}
| Result property | When it is set |
|---|---|
DroppedDocumentEntries |
Document-level entries that couldn't be combined. Info (title, author and so on) is dropped from every merge of two or more sources. |
RemovedDestinationCount |
Links whose target page is no longer in the output. |
RemovedOutlineEntryCount |
Bookmarks removed because nothing they point to survived. |
StructureTreeDropped |
The source was a tagged (accessible) PDF. The output isn't tagged and doesn't claim to be. |
FormDropped |
Two or more sources had interactive forms, which can't be merged because field names would clash. The fields' boxes still appear on the page. A form from a single source is kept, reduced to the fields whose pages survived. |
EncryptionRemoved |
A source was encrypted. See below. |
HasLosses |
Any of the above. |
Warning
StrictMode refuses almost every merge. Because merging two or more files always drops Info, HasLosses is true and a strict merge throws UnsupportedFeatureException. Use StrictMode for single-source work (delete, extract, reorder, split), where a loss really means content was dropped. For a merge, check DroppedDocumentEntries and the other properties yourself.
PDF/A conformance is never claimed for an assembled file, even when every source was PDF/A.
Encrypted sources give unencrypted output
using DocWright.Formats.Pdf;
using (var assembly = new PdfPageAssembly())
{
int statement = assembly.AddSource("encrypted-statement.pdf", password: "secret");
assembly.AppendAll(statement);
PdfAssemblyResult result = assembly.Save("statement-copy.pdf");
// The copy has NO password: DocWright decrypts, but doesn't encrypt.
Console.WriteLine($"encryption removed: {result.EncryptionRemoved}");
}
The pages have to be decrypted to be copied, and DocWright can't encrypt a PDF, so anyone can open the output without a password. Check EncryptionRemoved, or use StrictMode, before writing the file somewhere you wouldn't store unprotected content. PdfAssemblyOptions.EnforcePermissions refuses a source whose permission flags forbid both modifying and assembling it. It is off by default, because those flags are enforced by viewer applications, not by the file itself.
Options
PdfAssemblyOptions |
Default | Use it to… |
|---|---|---|
StrictMode |
false |
Throw instead of reporting a loss. |
Diagnostics |
null |
Receive the DXP801x diagnostics as they happen. |
EnforcePermissions |
false |
Refuse sources whose permissions forbid modifying and assembling them. |
Limits |
defaults | Cap MaxPages, MaxPdfObjectCount, MaxPdfNestingDepth, MaxInputBytes and MaxOutputBytes for untrusted input. See Hardening. |
Assembly only moves whole pages. Rotating, cropping, scaling, stamping, watermarking and printing several pages per sheet all change page content, and aren't offered. Assembling the same inputs twice gives byte-identical output.
Next steps
- Reading and rendering PDFs: page sizes, passwords and page images.
- Assemble a board pack: convert Word papers, then merge, extract, split and make thumbnails.