pdg XML → PDF

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.

Example Open in Playground
<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 an http(s) URL. In the browser it's fetched like an <image> source (relative paths resolve from the site root). Leave src off entirely for a fallback-only block (see below).
  • pages — which pages to take, 1-based. Numbers and a-b ranges, separated by commas and/or spaces. "1-4,6, 8-10 20" imports 1, 2, 3, 4, 6, 8, 9, 10, 20, in that order, with duplicates removed. Omit pages to 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.

Example Open in Playground
<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:

Example Open in Playground
<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.