Introduction
Drag a page from the middle of one PDF into another, and the result opens instantly, renders correctly, and the text is still selectable. That is not an accident, and it is not because a PDF reader "figured it out" the way a browser gracefully handles broken HTML. It works because of how PDF files are built internally: as a graph of independently addressable objects, not a flat, page-shaped sequence of bytes. Understanding that structure explains why some naive approaches to combining or reordering PDFs produce a corrupted file, while a correctly built tool like a PDF page organizer can freely reorder, duplicate, delete, and recombine pages from multiple source documents without touching a single pixel of the content.
This article walks through the actual object model defined by the PDF specification: the page tree, the page dictionary, the content stream, the resource dictionary, and the cross-reference table that ties them together. Two companion articles build on this foundation — Duplex Scanning and the Reverse-Order Problem and PDF Page Rotation and Orientation, Explained — but this one is the foundation both rest on.
Everything in a PDF Is a Numbered Object
A PDF file is, at its core, a flat collection of "indirect objects," each identified by an object number and a generation number, written to the file as n g obj ... endobj. An object can be a dictionary, an array, a stream (a dictionary with a block of raw bytes attached), a string, a number, or a reference to another object. That last case — an indirect reference, written n g R — is what turns the flat list of objects into a graph: a page object does not contain its own content directly, it holds a reference to the object that does.
This indirection is deliberate. It lets two completely different parts of the document — say, a page dictionary and a bookmark outline entry — point at the exact same underlying object without duplicating it. It also means a PDF reader (or a library like pdf-lib, which the tools on this site use to manipulate PDFs entirely in the browser) can load a document by resolving references lazily, only reading the objects it actually needs, rather than parsing the entire file into memory as one linear document. The full grammar for objects, streams, and references is defined in the PDF specification itself — ISO 32000-1, freely published by Adobe as the baseline PDF 1.7 reference — and every well-formed PDF file, regardless of which application produced it, follows this same object model.
The Page Tree: How a PDF Organizes Its Pages
At the root of every PDF's object graph sits the document catalog, referenced from the file's trailer. The catalog holds, among other things, a reference to the page tree — the structure that determines which pages exist and in what order. The page tree's root is a /Pages node, itself a dictionary with a /Kids array. Each entry in that array is either a leaf /Page object or another intermediate /Pages node, so large documents can nest the tree several levels deep to keep any single node's Kids array small — a balance that matters for readers that want to jump straight to, say, page 400 of a 500-page document without walking every sibling first.
The order pages appear in the final document is determined entirely by their position in this tree — specifically, by an in-order traversal of the /Kids arrays. Nothing about a page's own dictionary says "I am page 12." A page's number is purely positional, derived from where its object sits in the tree at render time. This single fact is the entire mechanism behind page reordering: change the order of entries in a /Kids array, and you have changed the document's page order, without touching the page objects, their content, or anything they reference.
Inside a Page Dictionary
Each leaf in the page tree is a /Page object — a dictionary describing one page's geometry, appearance, and content. It does not contain drawing instructions itself; instead it references the objects that do. The keys that matter most for understanding page manipulation are:
| Key | Purpose |
|---|---|
/Parent | Indirect reference back to the page's node in the page tree. |
/MediaBox | The page's physical boundary — the paper size, in points, as four coordinates. |
/Resources | A dictionary of the fonts, images, and other assets the page's content stream is allowed to reference by name. |
/Contents | A reference (or array of references) to the content stream(s) holding the actual drawing operators. |
/Rotate | Optional integer, a multiple of 90, telling viewers to rotate the page clockwise on display without touching its content — covered in detail in our rotation article. |
Notice what is not in this list: a page number. Because a page's identity is positional (its place in the tree) rather than intrinsic (a value stored on the object), copying a page object into a new document, or moving it to a different position in the same document's tree, is a structural operation on the tree — not an edit to the page's own data.
The Content Stream: What Actually Gets Drawn
The object a page's /Contents entry points to is a stream: a dictionary (holding metadata like the stream's length and compression filter) paired with a block of raw bytes. Those bytes, once decompressed — almost always with the FlateDecode filter, the same DEFLATE algorithm used by zip and gzip — are a sequence of PDF drawing operators: BT/ET bracket a block of text, Tf selects a font and size, Tj shows a string, re defines a rectangle, f fills the current path, and so on. A PDF content stream is, in effect, a small drawing program: a flat script of "move here, draw this, set that color" instructions with no notion of pages, documents, or ordering. It only knows how to paint one page's worth of a coordinate space.
This is the layer where text stays selectable. The Tj and related text-showing operators carry the actual character codes (mapped through the font's encoding back to real Unicode text), which is why copying a page at the object level preserves searchable, selectable, copy-pasteable text — the content stream bytes are copied verbatim, never re-rendered into pixels and never re-interpreted.

The Resource Dictionary: Fonts, Images, and Shared Assets
A content stream's drawing operators reference fonts and images by short names — /F1, /Im0 — rather than embedding them inline. Those names are resolved through the page's /Resources dictionary, which maps each name to the actual font or image object elsewhere in the file. This indirection is what makes PDFs reasonably compact: a document with a hundred pages of the same body font references that one font object a hundred times rather than embedding a hundred copies of it.
It also matters enormously for tools that copy pages between documents. When a page-organizing tool copies a page, it must not just copy the page dictionary and its content stream — it has to walk the entire dependency graph the content stream relies on: the resource dictionary, every font it names, every image XObject it names, and anything those objects in turn reference (a font's embedded font-program stream, an image's color-space object, and so on). Miss any link in that chain, and the copied page will reference an object that does not exist in the new document — the classic cause of a page that opens with missing fonts or a blank image box after a naive copy.
The Cross-Reference Table: Finding Objects Without Reading Everything
All of this — objects, the page tree, content streams, resource dictionaries — is scattered throughout the file in whatever order the authoring application happened to write them. Finding any particular object by number would mean scanning the entire file byte by byte, which is impractical for large documents. PDF solves this with a cross-reference table (or, in PDF 1.5 and later, a more compact cross-reference stream): a table, usually near the end of the file, mapping every object number to its exact byte offset. A short trailer dictionary at the very end points to the catalog (the entry point into the page tree) and to this cross-reference table, so a reader can open a 500-page PDF and jump directly to page 300's objects without parsing pages 1 through 299 first.
Every time a PDF's object graph changes — a page reordered, an object added or removed — a valid file needs a cross-reference table whose offsets are still accurate, and a trailer that still resolves. This is the detail that makes naive file surgery on PDFs so unreliable, and it is the subject of the next section.
Why Byte Concatenation Breaks Both Documents
It's tempting to think you could combine two PDFs by simply gluing their raw bytes together — after all, that works for plain text files. It does not work for PDFs, and the object model above explains exactly why. Each source PDF is a self-contained object graph: it has its own object numbering starting near 1, its own single cross-reference table with byte offsets computed relative to that file's layout, and its own single trailer identifying its own catalog. Concatenate the raw bytes of two such files, and you get:
- Colliding object numbers. Both files likely have an object 1, an object 2, and so on. A reader trying to resolve "object 1" in the combined file has no way to know which document's object 1 was meant.
- Stale byte offsets. The second file's cross-reference table records offsets measured from the start of its own file. After the first file's bytes are prepended, every one of those offsets points to the wrong location — typically landing in the middle of the first document's data.
- An ambiguous or overwritten trailer. A PDF reader looks for the trailer, and hence the catalog, working backward from the end of the file. Only one trailer survives as "the" entry point, so only one document's page tree is ever reachable — the other document's pages exist as unreferenced bytes the reader has no path to.
The result is not "the two documents, joined." It is one badly corrupted file whose object references point at the wrong places, opened (if it opens at all) by whichever reader is permissive enough to guess its way past the damage.
Why Object-Level Copying Works
The correct approach — the one pdf-lib implements and the one behind this site's PDF organizer — never touches raw bytes at all. It works purely at the object-graph level:
- Parse each source document into its actual object graph in memory — resolving the trailer, the catalog, and the page tree properly, the way any conformant reader would.
- Walk the dependency graph of each requested page: the page dictionary, its content stream, its resource dictionary, and every font, image, and nested object those reference.
- Copy that entire subgraph into a brand-new destination document, assigning fresh object numbers that cannot collide with anything already in the destination — because the destination started empty and is building its own numbering from scratch.
- Attach the copied page objects to the destination's own single, coherent page tree, in whatever order was requested.
- Rebuild a single cross-reference table and trailer for the destination document from its final, consistent object graph, once, when the file is saved.
Because the content stream bytes themselves are copied verbatim rather than reinterpreted, nothing is re-rendered and nothing is re-compressed — there is no quality loss, and no generation loss the way there would be if a page were rasterized to an image and reinserted. The pdf-lib PDFPage API that performs this copy also caches shared resources per copy operation, so if five pages of a source document share the same embedded font, that font is embedded into the destination once — not five times.
What This Means When You Reorder, Split, or Combine Pages
Once you see pages as tree positions rather than baked-in properties, every common page operation reduces to the same underlying move: rearrange entries in a /Kids array, or copy a subgraph into a new tree. Reordering two pages swaps two entries. Deleting a page removes an entry (and, if nothing else references its content, that content simply becomes unreachable and is dropped when the file is next saved). Splitting a document into two files copies disjoint subsets of the page tree into two fresh destination documents. Combining documents — whether end to end or interleaved page by page, as covered in our duplex scanning article — copies pages from multiple sources into one destination tree in whatever sequence the user chooses.
This is also why a page's rotation travels with it intact through any of these operations. Because /Rotate lives on the page dictionary itself rather than being baked into the content stream, copying, reordering, or combining pages carries that rotation value along for free — our companion piece, PDF Page Rotation and Orientation, Explained, covers exactly how that value behaves and why it must be added to rather than replace whatever rotation a page already carries.
Conclusion
A PDF is not a sequence of page-shaped byte ranges you can cut and paste; it is a graph of numbered objects, tied together by references, entered through a page tree, and indexed by a cross-reference table so any object can be found without reading the whole file. That structure is exactly what makes reordering, splitting, and recombining pages a lossless, purely structural operation when done at the object level — and exactly why gluing raw file bytes together produces a broken document instead. The next time you drag a page into a different position, delete one, or merge two scans into a single file with a tool like our PDF organizer, that operation is rewriting a small tree of pointers — not touching a single letter of the text underneath.