Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Fpdf Laravel Package

setasign/fpdf

FPDF is a free PHP class for generating PDF files with pure PHP. Clone of official releases, installable via Composer as setasign/fpdf. No active development in this repository.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the runtime. FPDF is a single global class with no service provider, facade, or config file — Composer autoloading is the entire integration.

composer require setasign/fpdf:^1.9
$pdf = new \FPDF('P', 'mm', 'A4');   // orientation, unit, page size ('A4', 'Letter', [w,h])
$pdf->AddPage();
$pdf->SetFont('Helvetica', '', 12);
$pdf->Cell(0, 10, 'Invoice #1001', 0, 1);
$pdf->Output('D', 'invoice.pdf');    // D = download, I = inline, S = return string, F = write file

That is a working controller. There is nothing else to wire up — no config/fpdf.php, no php artisan vendor:publish.

Where to look first. Do not read the package README for guidance; it is a release mirror and says so explicitly. The authoritative reference is the upstream manual at fpdf.org/en/doc/index.htm — the API surface you will use day-to-day is small and stable:

  • Positioning: SetXY, GetX, GetY, SetMargins, SetAutoPageBreak
  • Drawing: Cell, MultiCell, Write, Image, Rect, Line
  • Styling: SetFont, SetFillColor, SetDrawColor, SetTextColor, SetCellPadding
  • Structure: AddPage, PageNo, AliasNbPages, SetTitle / SetAuthor / SetCreator
  • Output: Output($dest, $name, $isUTF8)

The other file worth opening once is vendor/setasign/fpdf/fpdf.php — it is ~1,800 lines of readable, procedural-style PHP. When layout behaves oddly (GetY after a MultiCell, a header that shifts everything down), reading the method you called is faster than any Stack Overflow answer.

First use case to build. A fixed-format invoice or receipt: known column layout, variable rows, a header with your logo, a footer with Page X of Y. It exercises every primitive you will reuse — cells, colours, images, page breaks, metadata — and it is the document type FPDF is actually good at. Build it inside a dedicated class from the first commit (see below); retrofitting the wrapper later is the expensive mistake.

A 15-line smoke test before anything else. Verify the environment produces a valid PDF and that GD/FreeType are present if you plan to embed fonts or draw transparent PNGs:

$pdf = new \FPDF();
$pdf->AddPage();
$pdf->SetFont('Helvetica', 'B', 16);
$pdf->Cell(0, 20, 'PDF pipeline OK', 1, 1, 'C');
Storage::put('smoke/test.pdf', $pdf->Output('S'));   // 'S' returns the bytes, does not echo

If Output('S') returns '', you almost certainly have output leaking before the call — see Gotchas.

Implementation Patterns

Wrap it, immediately. FPDF has no framework coupling, which means without discipline the PDF logic lands in a controller and becomes the least-changeable code in the app. One contract, one implementation, call sites never touch \FPDF directly:

// app/Contracts/InvoiceRenderer.php
interface InvoiceRenderer {
    public function render(Invoice $invoice): string;   // raw PDF bytes
}

// app/Pdf/FpdfInvoiceRenderer.php
final class FpdfInvoiceRenderer implements InvoiceRenderer
{
    public function render(Invoice $invoice): string
    {
        $pdf = new InvoiceDocument($invoice);   // subclass, see below
        $pdf->AddPage();
        // ... layout
        return $pdf->Output('S');
    }
}

This is the single decision that preserves your exit to mPDF, Dompdf, or Browsershot. It costs an hour on day one and a rewrite on day 400.

Subclass for shared chrome. Headers and footers are invoked automatically by AddPage() and must be overridden, not composed:

class InvoiceDocument extends \FPDF
{
    public function __construct(private Invoice $invoice)
    {
        parent::__construct('P', 'mm', 'A4');
        $this->SetMargins(15, 25, 15);
        $this->SetAutoPageBreak(true, 25);
        $this->AliasNbPages();          // enables {nb} in the footer
        $this->SetTitle("Invoice {$invoice->number}");
        $this->SetAuthor(config('app.name'));
        $this->SetCreator('Acme Billing');
    }

    public function Header(): void   { /* logo via Image(), title, rule */ }
    public function Footer(): void
    {
        $this->SetY(-18);
        $this->SetFont('Helvetica', 'I', 8);
        $this->Cell(0, 8, 'Page '.$this->PageNo().' of {nb}', 0, 0, 'C');
    }
}

Keep Header() and Footer() free of side effects — no logging, no DB queries, no echo. They run on every AddPage, including the ones triggered mid-table by the auto page break.

Table rows that wrap. This is the pattern you will write hundreds of times. MultiCell with ln = 0 advances X, not Y, so drive the row manually and remember that the row height is unknown until the longest cell has been wrapped:

$widths = [20, 70, 20, 25, 25, 20];   // must sum to the usable width
foreach ($invoice->lines as $line) {
    $cells = [$line->sku, $line->description, $line->qty, ...];
    $height = 6;
    foreach ($cells as $i => $text) {
        $lines = max(1, ceil($this->GetStringWidth($text) / ($widths[$i] - 4)));
        $height = max($height, $lines * 5);
    }
    $x = $this->GetX(); $y = $this->GetY();
    foreach ($cells as $i => $text) {
        $this->MultiCell($widths[$i], 5, $text, 0, 'L');   // leaves cursor at end of block
        $this->SetXY($x + array_sum(array_slice($widths, 0, $i + 1)), $y);
    }
    $this->SetXY($x, $y + $height);
}

Check for a page break before drawing a row ($this->GetY() + $height > $this->PageBreakTrigger → AddPage()), otherwise MultiCell will split a row across pages and leave orphaned half-rows.

Stream versus store. Never hold a large PDF in the response cycle when you do not have to.

// Small, synchronous download
return response($bytes, 200, [
    'Content-Type'        => 'application/pdf',
    'Content-Disposition' => 'attachment; filename="invoice.pdf"',
]);

// Or let Laravel stream it
return response()->streamDownload(fn () => print $pdf->Output('S'), 'invoice.pdf');

// Anything non-trivial: queued job, write to object storage
Storage::disk('s3')->put("invoices/{$invoice->number}.pdf", $bytes);
Mail::to($invoice->customer)->queue(new InvoiceMail($invoice));

Generation in a queued job means the request cycle never touches SetFont. Give the job a failed() handler — a malformed image path or an unwritable font cache will throw inside a worker where nobody sees it.

Fonts and Unicode — decide once, up front. Core fonts (Helvetica, Times, Courier, Symbol, ZapfDingbats) are Latin-1 only and need no files. For anything else, embed a TTF:

$pdf->AddFont('DejaVu', '', public_path('fonts/DejaVuSans.ttf'));
$pdf->SetFont('DejaVu', '', 10);

FPDF 1.8+ generates a font definition cache file next to the TTF on first use, so the directory must be writable in that environment. In a read-only container this fails on the first request — generate the definitions at build time (or commit them) and load the cached file instead.

Encoding is your problem, not FPDF's. Strings handed to Cell/MultiCell must be Windows-1252, not UTF-8. Wrap the conversion once:

private function txt(string $s): string
{
    return iconv('UTF-8', 'Windows-1252//TRANSLIT', $s);
}

Apply it in a base class and forget it exists. Every template that hardcodes '€' or a curly apostrophe will silently produce ? on some page.

Testing. Treat the PDF bytes as a snapshot artifact. Two layers pay for themselves:

// 1. Structural: the bytes are a valid PDF
$this->assertStringStartsWith('%PDF-1.', $renderer->render($invoice));

// 2. Content: extract text and assert on it (smalot/pdfparser, or pdftotext in CI)
$text = (new \Smalot\PdfParser\Parser())->parseContent($bytes)->getText();
$this->assertStringContainsString('INV-1001', $text);
$this->assertStringContainsString('£1,250.00', $text);

Golden-file/visual diffing is worth it once you have more than two templates — a refactor in the layout helper will move every line item by 2mm and nothing else will tell you.

Gotchas and Tips

"FPDF error: Some data has already been output." The classic, and it is never FPDF's fault. A BOM in a PHP file, a stray newline after a closing ?>, whitespace before <?php, or a debug dump() earlier in the stack. FPDF writes directly to the output buffer in I/D mode. Two defences: use Output('S') and return the string through Laravel's response layer (so headers are never FPDF's concern), and delete closing ?> tags in your document classes.

MultiCell moves X, not Y — and does not reset. With $ln = 0 (the default) the cursor stays to the right of the block; with $ln = 1 it moves below but not back to the left margin; with $ln = 2 it goes below and back to the left margin. Most "my layout is drifting right" bugs are a missing $ln = 2 or a stale SetXY. Always capture GetX()/GetY() before drawing and restore explicitly.

Cell(0, ...) means "extend to the right margin" — not "full page width" and not "auto-size to the text". It is relative to the current left margin, which AddPage() resets and SetLeftMargin() changes. If a cell overflows the page, check the margin before the width.

Footer() and AliasNbPages(). {nb} is only substituted correctly if AliasNbPages() was called before the first AddPage(). Call it in the constructor. Also, $this->SetY(-18) in Footer() is relative to the bottom of the page, so it is negative by convention, not a mistake.

Auto page break fires inside MultiCell. It can split a MultiCell block across pages at an arbitrary line. For rows you must keep intact (invoice line items, signature blocks), set SetAutoPageBreak(false) around the row, measure first, and call AddPage() yourself. Do not rely on the automatic trigger for anything structural.

Font paths and FPDF_FONTPATH. FPDF looks in its own font/ directory for core font definitions by default. If you define('FPDF_FONTPATH', ...) after the class is loaded, or define it inconsistently across environments, you get FPDF error: Font not found in staging only. Prefer AddFont() with explicit paths over global constants.

Output('F', $path) and Output('S') are mutually exclusive in spirit. Once you call Output() with I/D, the PDF has been sent and the instance is done. Do not call Output() twice on the same object expecting two independent responses, and do not reuse an \FPDF instance across documents — headers, PageNo(), and the alias counter all carry state.

Images. JPEG and PNG are parsed natively; GIF and anything you want resampled needs GD. Pass one dimension and FPDF preserves the aspect ratio; pass both and it stretches, which is how logos end up squashed. Paths are resolved relative to the process working directory, not the app root — use public_path() or storage_path() absolutely, especially in queue workers where the CWD differs.

Metadata and dates. SetCreationDate() and SetModificationDate() exist, but the creation date defaults to "now", which means your snapshot tests will differ on every run. Set both explicitly in the constructor if you are diffing output, otherwise your CI is nondeterministic.

Links are cheap, footnotes are not. External links are a parameter on Cell/Write (the eighth argument). Internal links need AddLink() → SetLink() → Link(). Both are usable, but there is no automatic "continued on page X" mechanism — build that logic yourself if you need it.

The library is frozen; PHP is not. This repo mirrors upstream releases and takes no patches. Pinning ^1.9 is correct, but add a CI matrix job on your target PHP version and treat deprecation notices from fpdf.php as a real finding — a PHP upgrade that trips a dynamic-property or null-argument deprecation in a dependency with no upstream backlog is your problem to patch or fork. Budget the option to composer patch / vendor a one-line fork.

Extension points are subclass overrides, and that is the API. Header, Footer, AddPage, _putfonts, _putimages are the supported hooks. If you find yourself reaching for _beginpage or other underscore-prefixed internals, you are one upstream release away from a silent break — which, given the mirror has no releases to speak of, means you will not even get a changelog warning.

Do not attempt HTML. No HTML parsing, no CSS, no text flow. If a stakeholder asks for a layout "like the website", that is a Browsershot/Dompdf conversation, and the correct answer is a second renderer behind the same InvoiceRenderer contract, not a workaround inside FPDF.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel