# Low level PDF objects in C#

> Read and change any part of a PDF from C# with PdfDictionary, PdfArray, PdfStream and friends, for what the high level API does not cover. MIT.

Available since 1.1.

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

## 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`.

```csharp
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

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.

## Read

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.

```csharp
// /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`](https://www.jadev-corp.com/guides/extract-images/). Damaged data, or data over the document's decode
limit, raises `PdfFormatException`.

## Write

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.

```csharp
// 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](https://www.jadev-corp.com/guides/drawing/) does the same job with top left coordinates and keeps track
of resources for you; see [Coordinates and units](https://www.jadev-corp.com/concepts/coordinates-and-units/).

## 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

- 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.

## Related

- [Edit pages](https://www.jadev-corp.com/guides/edit-pages/): the high level editing API
- [Drawing](https://www.jadev-corp.com/guides/drawing/): content streams without writing operators by hand
- [Architecture](https://www.jadev-corp.com/concepts/architecture/): how the parser and writer use these types
- API: [PdfDictionary](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfdictionary/), [PdfArray](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfarray/), [PdfStream](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfstream/), [PdfReference](https://www.jadev-corp.com/api/pdfnetstack-objects/pdfreference/), [PdfDocument](https://www.jadev-corp.com/api/pdfnetstack/pdfdocument/)

Source: https://www.jadev-corp.com/pdfnetstack/guides/low-level-objects/
