Skip to content

Low level PDF objects in C#

Since 1.1View as Markdown

Reach every dictionary, array and stream of a PDF when the high level API does not cover what you need.

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 types live in the PdfNetStack.Objects namespace and mirror ISO 32000-1, section 7.3.

TypeHoldsCreate with
PdfNullThe null objectPdfNull.Instance
PdfBooleantrue or falsePdfBoolean.True, PdfBoolean.Get(value)
PdfNumberAn integer or a real, kept as canonical text so values read from a file survive a round trip exactlyPdfNumber.FromInteger(42), PdfNumber.FromDouble(0.5)
PdfNameA name such as /Type, without the slash in Valuenew PdfName("Type")
PdfStringBytes, which may be textPdfString.FromText("Hello"); read with ToText()
PdfArrayAn ordered listnew PdfArray(), PdfArray.Of(...)
PdfDictionaryKeys without the slash, in insertion ordernew PdfDictionary { ["Type"] = new PdfName("Annot") }
PdfStreamA dictionary plus encoded dataPdfStream.FromData(bytes), Flate compressed unless compress: false
PdfReferenceA reference to an indirect object of one documentReturned 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.

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.

  • References belong to one document. Use Pages.Import to 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.