Skip to content
getnextpdf.com

The anatomy of a PDF file

Open any PDF in a plain text editor and the first thing you see is reassuring: a %PDF-1.x or %PDF-2.0 header. The last thing you see is %%EOF. Everything between those two lines is a tidy little machine for finding objects by number. This page is a dissection. We open the file, name each organ, and show how they connect.

It is the structural companion to two neighbours. What a PDF actually is treats the file as an object graph; incremental updates cover how it grows over time. This page stays close to the bytes — the physical regions a parser walks across, in the order they sit on disk.

You almost never need this to use a PDF. You need it the day one goes wrong. A file opens in one viewer and not another; a validator reports a “damaged cross-reference table”; a signed document suddenly fails to verify. None of those are mysteries once you can read the anatomy. They are a number that no longer matches a position, a region in the wrong place, or a tail that points at nothing.

Knowing the layout turns “the PDF is corrupt” into a diagnosis you can act on. It is the difference between shrugging at a black box and pointing at the exact byte that lies.

A conforming PDF has four physical parts, in this file order (Spec: ISO 32000-2, §7.5.1):

  1. A header — one line, %PDF-2.0, naming the version.
  2. A body — the bulk of the file: a run of numbered indirect objects.
  3. A cross-reference section — an index from object number to the byte offset where that object lives. Classic PDFs use a text table; PDF 2.0 uses a compressed xref stream.
  4. A trailer — a small dictionary naming the entry point, followed by startxref, an offset, and %%EOF.

The twist: a reader does not start at the top. It starts at the bottom, reads startxref to find the index, and uses that index to reach any object directly. The file is written front to back but read back to front.

Let us walk the four regions in order, with the bytes in front of us.

The header is one line. NextPDF writes %PDF-2.0, and conventionally a second comment line of high-bit bytes so naive transfer tools treat the file as binary, not text. That second line is why a PDF opened as plain text shows a little garble right after the version.

The body is where the document lives. Each indirect object is a number, a generation, the keyword obj, a value, and endobj (Spec: ISO 32000-2, §7.3.10). The value is one of a few shapes — but two carry almost all the weight:

  • A dictionary, << /Key value … >>, is a map of names to values. The page tree, the catalog, the font descriptors: all dictionaries.
  • A stream is a dictionary followed by stream, a block of arbitrary bytes, and endstream. Page content, embedded fonts, and images are streams, almost always compressed. (Their filters are a story of their own, told in streams and filters.)

Objects point at each other by indirect reference2 0 R means “object 2, generation 0.” That is the wiring that turns a flat list of objects into a graph.

The cross-reference section is the part most people never picture correctly. In the classic form it is plain text: the keyword xref, then subsections of fixed-width 20-byte lines (Spec: ISO 32000-2, §7.5.4). Each line is a ten-digit byte offset, a five-digit generation, and a single flag — n for in-use, f for free — padded to exactly twenty bytes so a reader can seek to any entry by arithmetic alone. PDF 2.0 replaces this with a cross-reference stream: the same index, but binary and compressed inside a stream object marked /Type /XRef (Spec: ISO 32000-2, §7.5.8). Smaller, and able to describe objects packed inside object streams.

The trailer is the file’s table of contents (Spec: ISO 32000-2, §7.5.5). It names /Root — the document catalog, the single object from which everything else hangs — and /Size, the object count. Then comes the handshake that makes backward reading possible: startxref, a byte offset on its own line, and %%EOF. A reader seeks to the end, reads that offset, jumps straight to the cross-reference section, and is off.

  1. HeaderOne line, %PDF-2.0, naming the version. A binary-marker comment usually follows.
  2. BodyNumbered indirect objects — dictionaries and streams — referenced by N G R.
  3. Cross-reference sectionA text table of 20-byte entries, or a compressed /Type /XRef stream in PDF 2.0.
  4. TrailerNames /Root and /Size, then startxref + offset + %%EOF.
  5. Read orderA reader starts at %%EOF, follows startxref to the index, then reaches each object directly.
The four physical regions of a PDF in file order, and the path a reader actually takes through them — starting at the trailer and working inward via the cross-reference section.

There is a fifth region a long-lived file grows: an incremental update. A change is not written in place. The changed objects, a fresh cross-reference section, and a new trailer are appended after the first %%EOF, and that new trailer carries /Prev — the offset of the previous cross-reference section (Spec: ISO 32000-2, §7.5.6). The sections form a backward chain; for any object number, the newest entry wins. Because the original bytes never move, the cryptographic check over the byte range a signature actually covers still holds after an update. A later incremental update can still change what a validator reports about the document as a whole — whether the post-signature changes are permitted, and what the signature is taken to certify — but it cannot alter the signed bytes themselves. That property is the whole subject of incremental updates.

Here is the entire anatomy in one minimal file. The numbers under xref are byte offsets, and they have to be exact — point one character past where an object begins and a strict reader gives up.

%PDF-2.0
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
186
%%EOF

Read it the way a parser does. Last line: %%EOF. Above it: startxref 186, so seek to byte 186, where xref begins. The table says object 1 lives at byte 9. The trailer’s /Root 1 0 R points there — the catalog — and from the catalog you walk /Pages to the page tree and find the single page. Object 0 is always the free-list head with generation 65535, a fossil from the format’s first design that every reader still expects to see.

The trap is reading a PDF like a story — top to bottom, in order. It is not a story; it is an index with a backward pointer. Object numbers need not be sequential in the file, objects can appear in any physical order, and a reader never relies on their position. The only authoritative map is the cross-reference section, and the only way to find that map is the startxref offset at the very end.

The consequence surprises people. A PDF with a flawless body and one wrong digit in startxref is unreadable — the reader cannot find the index. A PDF with its objects in scrambled order but a correct cross-reference section is perfectly fine. Physical position carries no meaning. The recorded position carries all of it.

This page describes physical structure, not page content. How marks land on a page — content-stream operators, text showing, graphics state — is a separate subject. It also describes a well-formed file. Real-world PDFs are frequently a little broken and survive only because forgiving viewers rebuild the cross-reference table by scanning for obj keywords. That salvage is a viewer behaviour, not something the format guarantees.

Reading and repairing arbitrary third-party PDFs — edition availability
EditionAvailability
CoreNextPDF is a writer. It records every offset from the output buffer at the moment each object is emitted, so the files it produces have a cross-reference section that matches the body by construction.
ProParsing, reconstructing, or repairing a damaged cross-reference table in a file NextPDF did not write is out of scope across every edition.
EnterpriseFor inspection of an existing file’s structure, use a dedicated parser or validator; NextPDF guarantees correctness for what it writes, not for what it reads.

Why does a PDF have a binary-marker line after the header? Some older transfer tools would mangle a file they thought was plain text. The high-bit comment makes the file look unambiguously binary, so it survives the trip unchanged.

Is the xref stream just a smaller table? Mostly, with one extra power. Beyond being compressed, an xref stream can describe objects stored inside object streams — entries the classic 20-byte text table has no way to express.

Can I have both a table and a stream in one file? A single revision uses one or the other. But a hybrid file can pair a classic table for old readers with a cross-reference stream for new ones, so each kind of reader finds an index it understands.

  • Header — the first line, %PDF-2.0, naming the version; usually followed by a binary-marker comment.
  • Indirect object — a numbered object in the body, written N G obj … endobj, where N is the object number and G the generation.
  • Dictionary — a << /Key value … >> map of names to values; the most common object shape.
  • Stream — a dictionary plus a block of bytes between stream and endstream, used for content, fonts, and images.
  • Cross-reference table (xref) — the index from object number to byte offset; a 20-byte-per-entry text table classically, a /Type /XRef stream in PDF 2.0.
  • Trailer — the dictionary that names /Root and /Size, located via the startxref offset at the file’s end.
  • Incremental update — changed objects, a new cross-reference section, and a new trailer appended after %%EOF, with /Prev chaining back to the prior section.