Beyond generating PDFs from scratch, dskripchenko/php-pdf can read existing
PDF files and combine them: append one document after another, pick
individual pages, and stamp a page from one document onto pages of another.
All of it is pure PHP and MIT-licensed — no FPDI, no external binaries.
- Reading a PDF
- Appending documents
- Selecting and reordering pages
- Embedding (stamping) a page
- Placement
- Encrypted input
- What the reader handles
- Limitations (v1)
use Dskripchenko\PhpPdf\Pdf\Reader\ReaderDocument;
$doc = ReaderDocument::fromBytes(file_get_contents('report.pdf'));
echo $doc->pageCount(); // number of pages
foreach ($doc->pages() as $page) {
printf("%.0f × %.0f pt\n", $page->width(), $page->height());
}A ReaderDocument resolves objects lazily, follows the cross-reference chain
(classic tables, XRef streams, and object streams), and falls back to a
full-file scan when the cross-reference is corrupt.
PdfMerger concatenates pages from one or more sources into a new document
(pdftk-style):
use Dskripchenko\PhpPdf\Pdf\Merge\{PdfMerger, PdfSource};
$bytes = PdfMerger::create()
->append(PdfSource::fromFile('cover.pdf'))
->append(PdfSource::fromFile('body.pdf'))
->append(PdfSource::fromFile('appendix.pdf'))
->toBytes();
file_put_contents('combined.pdf', $bytes);
// or: PdfMerger::create()->append(...)->toFile('combined.pdf');PdfSource accepts a file path or raw bytes:
PdfSource::fromFile('/path/to.pdf');
PdfSource::fromBytes($binaryString);Pass 1-based page numbers, in the order you want them in the output:
$bytes = PdfMerger::create()
->append(PdfSource::fromFile('a.pdf'), pages: [1, 3, 5]) // subset
->append(PdfSource::fromFile('b.pdf'), pages: [2, 1]) // reordered
->toBytes();Omit pages to take every page in reading order. Each output page keeps its
source geometry (MediaBox, CropBox, rotation).
stamp() draws a page from one document on top of already-appended pages — the
source page becomes a reusable Form XObject. Use it for watermarks, letterheads,
backgrounds, or placing a page as a figure.
$bytes = PdfMerger::create()
->append(PdfSource::fromFile('invoices.pdf')) // base pages
->stamp(
PdfSource::fromFile('watermark.pdf'),
page: 1, // which source page
onPages: null, // null = every output page
placement: Placement::fit(),
)
->toBytes();Target specific output pages with onPages (1-based):
->stamp(PdfSource::fromFile('logo.pdf'), page: 1, onPages: [1], placement: Placement::at(40, 40, 0.5))The base page's own content is preserved and drawn first; the overlay is drawn on top from a clean graphics state.
Page annotations and the document outline (bookmarks) are carried into the
merged output by default. Internal links and bookmark destinations —
including named destinations — are remapped to the new pages. A link or
bookmark whose target page is not part of the output is dropped; external
URI links are always kept.
PdfMerger::create()
->append(PdfSource::fromFile('a.pdf'))
->withoutAnnotations() // opt out of annotation carry-over
->withoutOutlines() // opt out of bookmark carry-over
->toBytes();Form-field widgets (AcroForm) and popup annotations are not carried.
stamp() combines existing PDFs. To place an imported page inside a document
you build with php-pdf — drawing your own text, watermarks, or graphics over or
under it — use PageImporter::intoDocument(). The imported page becomes a Form
XObject you position with Page::useFormXObject():
use Dskripchenko\PhpPdf\Pdf\Document;
use Dskripchenko\PhpPdf\Pdf\StandardFont;
use Dskripchenko\PhpPdf\Pdf\Merge\PageImporter;
use Dskripchenko\PhpPdf\Pdf\Reader\ReaderDocument;
$src = ReaderDocument::fromBytes(file_get_contents('contract.pdf'));
[$w, $h] = PageImporter::pageSize($src, 0);
$doc = new Document();
$page = $doc->addPage(customDimensionsPt: [$w, $h]);
$form = PageImporter::intoDocument($doc, $src, pageIndex: 0);
$page->useFormXObject($form, 0, 0, $w, $h); // imported page as background
$page->showText('DRAFT', 200, 400, StandardFont::Helvetica, 48); // your content on top
$doc->toFile('stamped-contract.pdf');The page's rotation and crop box are handled automatically. The imported page's fonts and images are copied into the output verbatim.
Placement controls how the embedded page (sized in points) maps onto the
target page:
| Factory | Behaviour |
|---|---|
Placement::fit() |
Scale to fit, preserving aspect ratio, centered. |
Placement::stretch() |
Fill the target page exactly (aspect ratio ignored). |
Placement::at($x, $y, $scale = 1.0) |
Lower-left corner at ($x, $y), scaled by $scale. |
Rotated source pages (/Rotate 90/180/270) are baked upright via the form's
/Matrix, so placement works in intuitive upright coordinates.
Encrypted sources are decrypted transparently on read and re-emitted unencrypted in the merged output:
PdfSource::fromFile('protected.pdf', password: 'secret');Supported: RC4 (40/128-bit), AES-128 (AESV2), and AES-256 (V5 R5/R6). Both the user and owner password are tried. An empty password (the common "owner-only restrictions" case) works without any argument. Predictor-encoded streams are decoded at any bit depth (8-bit and 16-bit, plus sub-byte for PNG predictors).
The public-key security handler (/Adobe.PubSec, certificate-based) is not
supported — such files raise a clear "Unsupported security handler" error
rather than producing garbage.
- Classic
xreftables, XRef streams (PDF 1.5+), hybrid/XRefStm, and incremental updates (/Prev). - Object streams (compressed objects).
- Filters: Flate (with PNG/TIFF predictors), LZW, ASCII85, ASCIIHex, RunLength. Image filters (DCT/JPX/CCITT/JBIG2) are copied through verbatim.
- Corrupt/missing cross-reference recovery by scanning object headers.
- Page-tree flattening with inherited MediaBox/CropBox/Rotate/Resources.
Merging rebuilds each page from its content and resources. The following are not carried into the output yet:
- Interactive form fields (AcroForm) and widget/popup annotations — dropped.
- Structure tags (Tagged PDF / PDF-A conformance of the result).
- Image and font streams are copied verbatim, never re-encoded.
Annotations and outlines (bookmarks) with internal/named destinations are carried and remapped — see Annotations and bookmarks.
These are documented so "supports merge" is not mistaken for "preserves
everything". See docs/design/pdf-merge.md for the full scope and roadmap.