elibyy/tcpdf-laravel
elibyy/tcpdf-laravel is a Laravel wrapper for TCPDF that adds a service provider, facade, and publishable configuration. It simplifies generating, customizing, and outputting PDFs from Laravel apps, so you can stream, download, or save documents with minimal setup.
Install and wire-up
composer require elibyy/tcpdf-laravel
php artisan vendor:publish --provider="Elibyy\TCPDF\ServiceProvider" --tag=config
On Laravel 5.5+ the provider and TCPDF alias are auto-discovered. On older skeletons (or if you disable discovery), register manually:
// config/app.php
'providers' => [ /* ... */ Elibyy\TCPDF\ServiceProvider::class ],
'aliases' => [ /* ... */ 'TCPDF' => Elibyy\TCPDF\Facades\TCPDF::class ],
Where to look first
config/tcpdf.php — the published config controls the constructor arguments (page_format, page_orientation, unit, unicode, encoding, diskcache, pdfa) plus default fonts and paths. This is the first file to read when output looks wrong.Elibyy\TCPDF\Facades\TCPDF — it is a thin proxy. Every method call is forwarded to an Elibyy\TCPDF\TCPDF instance, so the entire TCPDF 6.x API is available: Cell, MultiCell, writeHTML, Image, Output, etc.vendor/elibyy/tcpdf-laravel/src/TCPDF.php — shows exactly how config keys map to the parent constructor and to TCPDF constants. Read it before filing a "my config is ignored" issue.First use case — download an invoice
use Elibyy\TCPDF\Facades\TCPDF;
Route::get('/invoices/{id}/pdf', function ($id) {
TCPDF::SetTitle("Invoice #{$id}");
TCPDF::SetAuthor(config('app.name'));
TCPDF::AddPage();
TCPDF::SetFont('helvetica', '', 12);
TCPDF::Write(0, 'Hello from TCPDF');
return response(TCPDF::Output('invoice.pdf', 'S'))
->header('Content-Type', 'application/pdf');
});
Note the 'S' output mode: it returns the PDF as a string, which you hand to a normal Laravel response. 'I'/'D' echo bytes and send headers directly and are usually the wrong choice inside a framework (see Gotchas).
1. Choose the right Output() mode
| Mode | Behaviour | Use when |
|---|---|---|
S |
Returns string | Small/medium PDFs returned via response() |
F |
Writes file server-side | Large PDFs, queued generation |
D |
Forces download | Never inside controllers — use a response |
I |
Inline to browser | Only in a bare script with no middleware |
// Stream a large document without buffering the whole string
TCPDF::Output(storage_path("app/invoices/{$id}.pdf"), 'F');
return response()->download(storage_path("app/invoices/{$id}.pdf"))
->deleteFileAfterSend();
2. Blade → HTML → PDF (the most common real-world pattern)
TCPDF has no Blade engine, so render the view yourself and hand the HTML to writeHTML:
$html = view('pdf.invoice', ['invoice' => $invoice])->render();
TCPDF::AddPage();
TCPDF::SetFont('dejavusans', '', 10);
TCPDF::writeHTML($html, true, false, true, false, '');
return response(TCPDF::Output("invoice-{$invoice->id}.pdf", 'S'))
->header('Content-Type', 'application/pdf');
Keep the Blade view to tables and inline styles — TCPDF's CSS support is roughly CSS 2.1 with tables only. No flexbox, no grid, no external stylesheets.
3. Custom header/footer — subclass, then bind
Override Header()/Footer() on a subclass and put it in the container under the key the facade resolves (tcpdf) in a service provider:
namespace App\Pdf;
class BrandedPdf extends \Elibyy\TCPDF\TCPDF
{
public function Header(): void
{
$this->SetY(8);
$this->SetFont('helvetica', 'B', 9);
$this->Cell(0, 8, config('app.name'), 0, false, 'R');
}
public function Footer(): void
{
$this->SetY(-12);
$this->SetFont('helvetica', '', 8);
$this->Cell(0, 8, $this->getAliasNumPage().' / '.$this->getAliasNbPages(), 0, false, 'C');
}
}
// AppServiceProvider::register()
$this->app->bind('tcpdf', fn () => new \App\Pdf\BrandedPdf());
Call setHeaderFont()/setFooterFont() and set margins before the first AddPage(); after that they no longer apply.
4. One request, several PDFs
The facade resolves a single shared instance. For multi-document endpoints (e.g. batch statements), skip the facade and instantiate directly:
foreach ($customers as $customer) {
$pdf = new \Elibyy\TCPDF\TCPDF();
$pdf->AddPage();
// ...
$pdf->Output(storage_path("app/statements/{$customer->id}.pdf"), 'F');
}
If you insist on the facade, reset between documents:
\Illuminate\Support\Facades\Facade::clearResolvedInstance('tcpdf');
5. Per-document config overrides
Because the instance is built from config the first time it is resolved, mutate config before the first facade call:
config(['tcpdf.page_orientation' => 'L', 'tcpdf.page_format' => 'A4']);
TCPDF::AddPage();
Or pass parameters straight to TCPDF methods (SetMargins, setPageFormat, SetFont) — usually clearer than fighting the config layer.
6. Queue heavy generation
PDF rendering is CPU- and memory-bound. Generate 'F' to storage in a job, attach the path to a mailable, and keep the HTTP request thin. This also sidesteps the facade-state problem if you instantiate fresh instances per job.
7. Testing
Output('', 'S') is easy to assert against:
$pdf = TCPDF::Output('', 'S');
$this->assertStringStartsWith('%PDF-', $pdf);
The facade is stateful. Laravel's Facade caches the resolved instance for the lifetime of the process. In a single request, every TCPDF:: call mutates the same document — fine for one PDF, disastrous for two, and genuinely dangerous in queue workers where the process is reused across jobs. Fix: Facade::clearResolvedInstance('tcpdf') before reuse, or instantiate new \Elibyy\TCPDF\TCPDF() directly.
Never use Output(..., 'I') from a controller. It echoes bytes and emits raw headers, bypassing Laravel's response pipeline. You will get corrupted output with Debugbar, gzip middleware, Content-Length assumptions, or PHPUnit's output buffering. Always return 'S' in a response or 'F' + response()->download().
Absolute paths only. TCPDF resolves image and font paths relative to the current working directory, which differs between CLI (project root) and FPM (often public/). Always use public_path(), storage_path(), or an absolute path:
TCPDF::Image(public_path('img/logo.png'), 15, 10, 30, 0, '', '', '', true, 300);
K_TCPDF_EXTERNAL_CONFIG is set by the wrapper. TCPDF's own tcpdf_config.php is disabled so the Laravel config wins. When you see "undefined constant" or "font not found" errors, it is almost always the wrapper's config-to-constant mapping — check the published config and the vendor TCPDF.php before assuming a TCPDF bug.
Unicode needs a Unicode font. With 'unicode' => true and a UTF-8 document, helvetica will mangle anything outside Latin-1. Set SetFont('dejavusans', '', 10) (or freeserif, or add your own font) for accented characters, Cyrillic, CJK, and the € sign.
HTML is not HTML5. writeHTML supports tables, inline styles, <b>/<i>/<u>, <br>, limited <a>. No flexbox, no float-based layout tricks that depend on modern CSS, no <script>, and page breaks only via page-break-* on block elements (patchily). Prototype the HTML in a browser, but verify pagination in the PDF.
Font subsetting is the usual performance culprit. Large documents with many writeHTML calls get slow and memory-hungry. Mitigations: $pdf->setFontSubsetting(false) when you don't need it, enable 'diskcache' => true for big documents, raise memory_limit, and move generation to a queue.
Config cache freezes config/tcpdf.php. After php artisan config:cache, editing the file does nothing until you re-cache. Similarly, config([...]) overrides applied after the facade has resolved are silently ignored.
Header/Footer callbacks must never throw. An exception inside Header() or Footer() surfaces as a broken or blank page with little context. Wrap custom logic in try/catch and log.
PDF/A is a mode switch, not a checkbox. Setting 'pdfa' => true changes constructor behaviour and restricts output/encryption. Validate the result with an external PDF/A checker before shipping; do not assume the flag alone produces compliant files.
TCPDF 6.x is legacy. The wrapper pins the TCPDF line, which predates modern PHP. Expect deprecation notices on PHP 8.1+, and pin the wrapper version in composer.json — treat a minor bump as a regression-test event, since Output() signatures and font handling have shifted historically between TCPDF generations.
Extension points to remember: bind your own subclass under the tcpdf container key; override config at runtime before first resolution; use setPrintHeader(false) / setPrintFooter(false) for full-bleed cover pages; and reach for the underlying TCPDF object via TCPDF::getInstance()-style access only when the facade genuinely can't express what you need.
How can I help you explore Laravel packages today?