setasign/fpdi-fpdf
Setasign FPDI-FPDF combines FPDI with FPDF so PHP applications can import pages from existing PDFs and place them into new FPDF-generated documents. Useful for merging, watermarking, stamping, and templating PDFs without external tools.
setasign/fpdi-fpdf is not a library — it is an abandoned Composer metapackage whose only job was to pin compatible versions of setasign/fpdi and setasign/fpdf. Installing it gives you no classes of its own. Because it was archived in February 2020, the correct "first step" is to not install it and instead require the concrete packages directly:
composer remove setasign/fpdi-fpdf # if already present
composer require setasign/fpdi setasign/fpdf
Those two packages give you the actual runtime you care about:
setasign/fpdf — the classic FPDF generator (global FPDF class).setasign/fpdi — setasign\Fpdi\Fpdi, which extends FPDF and adds PDF importing (setSourceFile, importPage, useTemplate).Look first at the FPDI source in vendor/setasign/fpdi/src/Fpdi.php and the FpdiTrait; that is where 90% of the day-to-day API lives. There is no Laravel service provider, no config file, and nothing to publish.
Canonical first use case: you have an existing PDF (invoice, contract, certificate) and need to stamp text or a signature onto every page without regenerating the document.
use setasign\Fpdi\Fpdi;
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile(storage_path('app/invoices/1024.pdf'));
for ($i = 1; $i <= $pageCount; $i++) {
$tpl = $pdf->importPage($i);
$size = $pdf->getTemplateSize($tpl); // ['width'=>..,'height'=>..,'orientation'=>'P']
$pdf->AddPage($size['orientation'], [$size['width'], $size['height']]);
$pdf->useTemplate($tpl);
$pdf->SetFont('Helvetica', 'B', 12);
$pdf->SetTextColor(200, 0, 0);
$pdf->SetXY(10, $size['height'] - 20);
$pdf->Cell(0, 10, 'PAID', 0, 0, 'C');
}
file_put_contents(storage_path('app/invoices/1024-paid.pdf'), $pdf->Output('S'));
Output('S') returns the PDF as a string instead of writing to the browser — this is the form you want in a framework context.
Wrap it in a service, not in controllers. FPDI/FPDF are mutable objects with global-ish state; keeping construction and output in one place prevents "why is my font wrong on page 3" bugs.
namespace App\Services;
use setasign\Fpdi\Fpdi;
class PdfStamper
{
public function stamp(string $sourcePath, string $text): string
{
$pdf = new Fpdi();
$pdf->setSourceFile($sourcePath);
// ... loop as above ...
return $pdf->Output('S');
}
}
Bind it in a service provider if you want to swap it in tests, and inject it into controllers or jobs.
Return it as a download without touching the filesystem.
public function download(Order $order, PdfStamper $stamper)
{
$bytes = $stamper->stamp(
\Storage::path($order->invoice_path),
'PAID ' . now()->toDateString()
);
return response($bytes, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="invoice-'.$order->id.'.pdf"',
]);
}
Use response()->streamDownload() if the output can be large, and Storage::path() / Storage::disk()->path() rather than concatenating storage_path() by hand.
Push batch work into a queued job. Generating 500 stamped invoices in a request is a memory and timeout problem, not an FPDI problem. set_time_limit(0), raise memory_limit, write results to Storage, and let the job update a status column.
Merging multiple PDFs is the other common pattern. Loop the sources, call setSourceFile() per source, import each page, and append. There is no "append file" shortcut — every page is imported individually.
Testing: keep a couple of small fixture PDFs in tests/Fixtures and assert on %PDF- header plus page count (e.g. with a quick preg_match_all('/\/Type\s*\/Page[^s]/', $bytes)). Do not try to assert on rendered pixels.
Config quirks: none, by design. Units default to millimeters, page format to A4, and compression to on. Set these explicitly at the top of your service ($pdf->SetCompression(true)), because they are object state, not app config.
composer install will flag it; composer audit and Dependabot will nag. The fix is to require setasign/fpdi and setasign/fpdf directly and drop the metapackage. Your lockfile will otherwise carry a dead transitive node indefinitely.setasign/fpdf (the 1.8.x line) predates PHP 8 and can emit deprecation/notice chatter on 8.1+ (dynamic properties, implicit float-to-int). If it breaks under APP_DEBUG=false-style strict error handling, either raise it to the newest 1.8.x or silence notices around the call — do not patch vendor/.Helvetica, Times, Courier) are Latin-1 only. Emoji, CJK, or accented names will render as mojibake. You must add a TTF via AddFont() and use iconv/mb_convert_encoding for the text.setSourceFile() is stateful and returns the page count. Call it once per source document; calling it again mid-import resets the template registry and your previously imported page IDs become invalid.$tpl must not be reused. Re-import inside the loop for multi-document merges.useTemplate() usually respects this, but if content appears clipped, pass explicit offsets ($pdf->useTemplate($tpl, 0, 0, $w, $h, true)) and inspect getTemplateSize() rather than assuming portrait.72pt = 25.4mm.Output() modes differ. 'S' returns a string, 'D' sends headers, 'F' writes a file. Mixing 'I' with a Laravel Response produces a corrupted download because FPDF has already flushed headers.memory_limit; cap file size and page count at validation.FPDF class name. FPDF is not namespaced; FPDI is (setasign\Fpdi\Fpdi). If you already have another library defining FPDF (e.g. an old TCPDF shim), you get a fatal "cannot redeclare class". Check composer why setasign/fpdf before adding a second PDF stack.setasign/fpdi + FPDI_Protection is commercial; pdftk/qpdf via shell are alternatives).tecnickcom/tcpdf, the fpdi-tfpdf variant and this one cannot coexist cleanly. Resolve with composer why-not and pick one PDF engine.How can I help you explore Laravel packages today?