Low level PDF objects in C#
Reach every dictionary, array and stream of a PDF when the high level API does not cover what you need.
Short answer
Section titled “Short answer”doc.Catalog is the root of the object graph and page.Dictionary is a page’s own dictionary.
doc.Resolve follows references, doc.AddObject adds new indirect objects, and
doc.GetStreamData decodes a stream. Changes are written by the next Save.
PdfDocument doc = PdfDocument.Open("report.pdf");doc.Catalog.Set("PageMode", new PdfName("UseOutlines")); // open with the bookmarks panel shown
PdfPage page = doc.Pages[0];if (doc.Resolve(page.Dictionary["Resources"]) is PdfDictionary resources && doc.Resolve(resources["Font"]) is PdfDictionary fonts){ Console.WriteLine($"Fonts on page 1: {string.Join(", ", fonts.Keys)}");}The object types
Section titled “The object types”The types live in the PdfNetStack.Objects namespace and mirror ISO 32000-1, section 7.3.
| Type | Holds | Create with |
|---|---|---|
PdfNull | The null object | PdfNull.Instance |
PdfBoolean | true or false | PdfBoolean.True, PdfBoolean.Get(value) |
PdfNumber | An integer or a real, kept as canonical text so values read from a file survive a round trip exactly | PdfNumber.FromInteger(42), PdfNumber.FromDouble(0.5) |
PdfName | A name such as /Type, without the slash in Value | new PdfName("Type") |
PdfString | Bytes, which may be text | PdfString.FromText("Hello"); read with ToText() |
PdfArray | An ordered list | new PdfArray(), PdfArray.Of(...) |
PdfDictionary | Keys without the slash, in insertion order | new PdfDictionary { ["Type"] = new PdfName("Annot") } |
PdfStream | A dictionary plus encoded data | PdfStream.FromData(bytes), Flate compressed unless compress: false |
PdfReference | A reference to an indirect object of one document | Returned by doc.AddObject or found in the document |
A dictionary’s indexer returns null for a missing key, and setting null removes the entry.
GetName(key) returns a name value without its slash.
Values in dictionaries and arrays may be references. Pass them through doc.Resolve, which returns
the object itself when it is not a reference, and PdfNull.Instance for a reference to a missing
object.
// /Contents is one stream or an array of streams.PdfObject? contents = doc.Resolve(page.Dictionary["Contents"]);IEnumerable<PdfObject> parts = contents is PdfArray array ? array : [contents!];foreach (PdfObject part in parts){ if (doc.Resolve(part) is PdfStream stream) { byte[] content = doc.GetStreamData(stream); // filters undone, image codecs kept Console.WriteLine($"Content stream: {content.Length:N0} bytes decoded, {stream.Data.Length:N0} stored"); }}GetStreamData undoes the Flate, LZW, ASCIIHex, ASCII85 and RunLength filters and the PNG and
TIFF predictors. Image codecs (DCTDecode, JPXDecode, JBIG2Decode, CCITTFaxDecode) are left in
place, so an image stream comes back as its JPEG or JPEG 2000 file; for pixels, use
PdfPageImage.Decode. Damaged data, or data over the document’s decode
limit, raises PdfFormatException.
Streams are always indirect objects: add them with doc.AddObject and store the returned
reference. An added object is written on save only if something references it.
// A second content stream that draws a blue square over the page.PdfStream square = PdfStream.FromData("q 0 0 1 rg 72 72 144 144 re f Q"u8);PdfObject existing = page.Dictionary["Contents"]!;page.Dictionary.Set("Contents", PdfArray.Of(existing, doc.AddObject(square)));doc.Info["Reviewed"] = "yes";doc.Save("report-square.pdf");The square in this sample is placed in PDF default user space, from the bottom left of the page. The drawing canvas does the same job with top left coordinates and keeps track of resources for you; see Coordinates and units.
What is already on the object
Section titled “What is already on the object”page.Dictionary already carries the inherited attributes (resources, boxes, rotation), copied
onto each page when the document opens. doc.Catalog is the document catalog, where viewer
settings, page labels, named destinations, outlines and the form live. Most of these have a high
level API too: doc.Viewer, doc.PageLabels, doc.NamedDestinations, doc.Outlines and
doc.Form.
Limits
Section titled “Limits”- References belong to one document. Use
Pages.Importto move pages between documents rather than copying references. - Saving rebuilds the page tree as one flat node and writes only objects reachable from the catalog, the information dictionary and the document ID.
- The object model does not check what you write against the specification. A dictionary with a wrong key is saved as is.
Related
Section titled “Related”- Edit pages: the high level editing API
- Drawing: content streams without writing operators by hand
- Architecture: how the parser and writer use these types
- API: PdfDictionary, PdfArray, PdfStream, PdfReference, PdfDocument