Guide
Importing PDF pages
Splice pages from an existing PDF into your document with <pdf src= pages=>. Each source page becomes an output page at its own size — a real merge — with the source's fonts, images, and layout preserved byte-for-byte.
Sometimes the page you need already exists as a PDF — a signed contract, a
designed cover, a chart someone exported, a hundred-page appendix. You don't want
to redraw it; you want to include it. The <pdf> element does that: it takes
pages from an external PDF and drops them into your document as real pages.
<pages>
<page><!-- a cover you authored --></page>
<pdf src="brochure.pdf" pages="1-4,6, 8-10 20" />
<page><!-- your closing page --></page>
</pages><pdf> sits inside <pages>, right beside <page>. Each page it selects from
the source becomes one page in the output, at that source page's own size.
It is a merge, not a re-layout: pdg does not re-flow the imported content, it
copies it across intact.
Selecting pages
src— where the source lives: a file path on the CLI, or anhttp(s)URL. In the browser it's fetched like an<image>source (relative paths resolve from the site root). Leavesrcoff entirely for a fallback-only block (see below).pages— which pages to take, 1-based. Numbers anda-branges, separated by commas and/or spaces."1-4,6, 8-10 20"imports1, 2, 3, 4, 6, 8, 9, 10, 20, in that order, with duplicates removed. Omitpagesto import the whole document. A number past the last page is ignored rather than erroring.
Order follows what you write, so pages="3,1" imports page 3 then page 1. The
imported pages count toward the document's page total, so a
{pages} footer on your own pages still reads
correctly.
What is preserved
An imported page is spliced verbatim. Its text, embedded fonts, images, vector graphics, colours (including CMYK), page rotation, and crop box all come across exactly as they were — pdg copies the source page's content stream byte-for-byte and re-emits the objects it references. Rendered side by side, an imported page is pixel-identical to the original.
What an imported page does not get is your document's furniture: a
page template (<layout>) is not painted over imported
pages, and document-wide defaults don't reach inside them.
They are foreign pages, kept as they were — your header and footer belong on the
pages you authored, not on the contract you embedded.
Embedding the same source more than once
You can import from the same PDF as many times as you like — different <pdf>
blocks, the same or different pages — and it stays cheap. pdg copies each
source object once and references it wherever it's used, so embedding a
brochure twice, or importing overlapping page ranges, does not duplicate its
fonts or images in the output. A source page selected twice shares one copy of
its content.
<pages>
<pdf src="report.pdf" pages="1" /> <!-- the cover -->
<page><!-- a divider you authored --></page>
<pdf src="report.pdf" pages="1" /> <!-- the same cover again, no extra bytes -->
</pages>When the source can't be read — fallback
The bytes might not be there: no src, a missing file, a URL the browser
couldn't fetch, a file that isn't a usable PDF. Rather than fail the whole
render, <pdf> falls back to its own content, laid out as an ordinary page:
<pdf src="https://example.com/q3-report.pdf">
<vertical main="center" cross="center">
<text size="16" color="#888">The Q3 report is available separately.</text>
</vertical>
</pdf>If the source loads, the body is ignored; if it doesn't, the body is what you see. So a document always renders something sensible, and you control what that something is.
An empty <pdf> whose source can't be read is different: with nothing to
import and no fallback to show, it produces no page at all and records a
warning, rather than emitting a stray blank page. Give the block fallback content
whenever you want a placeholder page in its place.
How it works
A PDF is a random-access format — its table of contents (the cross-reference
table) sits at the end and points at the byte offset of every object. pdg uses
that: it never reads a source front-to-back into memory. It parses the
cross-reference table once, then seeks to just the pages and objects it
actually needs. A multi-gigabyte source is read within a small, bounded budget —
the same philosophy as streaming output, and the two compose:
pdg build --stream can import from a huge source and write a huge output,
holding neither in full.
The reader is also forgiving. Real-world PDFs are often subtly malformed — a stale offset after an edit, a missing trailer, a wrong length — and many viewers reject them. pdg first tries the conforming path (classic and modern cross-reference tables, object streams, incremental updates); if that fails or comes up short, it rebuilds the cross-reference table by scanning the file for objects and recovers the page list anyway. The goal is to read as much of a damaged document as it can, not to refuse it.
Importing is bounded-memory on the CLI, where pages are seeked off disk. In the browser there is no filesystem, so the source is fetched whole before rendering — fine for ordinary PDFs, and the same import otherwise.
Limitations
- Encrypted PDFs are not supported. A password-protected or encrypted source
is refused, and the
<pdf>falls back to its content. Decrypt it first if you need to import it. - Interactive form fields don't carry across. Page-level annotations (including link rectangles) are imported, but a source's form is defined at the document level, which pdg does not merge — so imported AcroForm widgets render as static content rather than fillable fields.
- No re-flow. An imported page keeps its original size and layout; pdg won't resize or reformat it to match your pages. That's the point — it's a faithful splice — but it means a landscape source page stays landscape in your portrait document.