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

Tcpdf Laravel Package

soqrate/tcpdf

A Laravel integration for TCPDF that simplifies generating PDFs from HTML, with configuration and service provider support for creating, saving, or streaming PDF documents in your application.

View on GitHub
Deep Wiki
Context7

Getting Started

soqrate/tcpdf is a Composer distribution of the classic TCPDF single-class PDF generator. It is not a Laravel-native wrapper — there is no service provider, no facade, no config publish. You get the TCPDF class and a set of K_* path constants, and you drive it imperatively. Treat it as a low-level rendering engine you wrap yourself.

Install and verify autoloading first.

composer require soqrate/tcpdf

Before writing any code, confirm how the package autoloads. TCPDF historically is not PSR-4 — it is a monolithic tcpdf.php plus a config/tcpdf_config.php. Check vendor/soqrate/tcpdf/composer.json for a classmap or files entry targeting the main class file. If it is absent, new TCPDF() will fatal with "class not found" and you must require_once the file yourself (ideally in a small bootstrap or a service provider's register()).

First use case: stream a PDF download from a controller.

public function show(Invoice $invoice)
{
    $pdf = new \TCPDF('P', 'mm', 'A4', true, 'UTF-8');

    $pdf->SetCreator(config('app.name'));
    $pdf->SetTitle("Invoice {$invoice->number}");
    $pdf->SetMargins(15, 20, 15);
    $pdf->SetAutoPageBreak(true, 25);

    $pdf->AddPage();
    $pdf->SetFont('helvetica', '', 11);
    $pdf->Write(6, 'Invoice ' . $invoice->number);

    return response($pdf->Output('invoice.pdf', 'S'), 200, [
        'Content-Type'        => 'application/pdf',
        'Content-Disposition' => 'inline; filename="invoice.pdf"',
    ]);
}

The 'S' output mode returns the PDF as a string — this is what you want in Laravel. Never use Output(..., 'I') or 'D' inside a controller: those emit raw headers and echo directly, bypassing the response stack, which breaks middleware, session handling, and testing.

Where to look first in the source:

  • tcpdf.php — the class itself; grep for function Header, function Footer, function Error, and public function Output.
  • config/tcpdf_config.php — the K_PATH_* and font defaults. Read this before your first render; it determines where fonts, cache, and images are resolved from.
  • fonts/ — bundled core fonts (helvetica, times, courier, dejavusans). Only these work out of the box.

Implementation Patterns

Subclass per document type, do not scatter TCPDF calls through controllers. Headers, footers, and page-numbering logic belong in a dedicated class. Laravel's container will happily resolve it.

namespace App\Pdf;

class InvoicePdf extends \TCPDF
{
    public function __construct(protected array $meta)
    {
        parent::__construct('P', 'mm', 'A4', true, 'UTF-8', false);
        $this->SetMargins(15, 35, 15);
        $this->SetHeaderMargin(10);
        $this->SetFooterMargin(15);
        $this->SetAutoPageBreak(true, 25);
        $this->setImageScale(PDF_IMAGE_SCALE_RATIO);
    }

    public function Header(): void
    {
        $this->SetY(12);
        $this->SetFont('helvetica', 'B', 9);
        $this->Cell(0, 5, $this->meta['company'], 0, 1, 'L');
        $this->SetDrawColor(200);
        $this->Line(15, 24, 195, 24);
    }

    public function Footer(): void
    {
        $this->SetY(-18);
        $this->SetFont('helvetica', '', 8);
        $this->Cell(0, 5, 'Page ' . $this->getAliasNumPage()
            . ' of ' . $this->getAliasNbPages(), 0, 0, 'C');
    }
}

getAliasNumPage() / getAliasNbPages() emit placeholder tokens that TCPDF resolves at output time — this is the only correct way to get "Page 3 of 12" without a second pass. Do not use getPageNum() in the footer; it captures the page number at the moment the footer renders, which is usually right but breaks if you re-render a page.

Render Blade to HTML, then hand it to writeHTML(). This is the workflow most teams settle on: designers work in Blade/CSS, and you keep a strict subset of HTML supported by TCPDF.

public function action(Invoice $invoice)
{
    $pdf = new InvoicePdf(['company' => config('app.name')]);
    $pdf->AddPage();

    $html = view('pdf.invoice', [
        'invoice' => $invoice,
        'logo'    => public_path('img/logo.png'),
    ])->render();

    $pdf->writeHTML($html, true, false, true, false, '');

    return response($pdf->Output('invoice.pdf', 'S'), 200, [
        'Content-Type' => 'application/pdf',
    ]);
}

Pass absolute filesystem paths for images into the view and use <img src="{{ $logo }}">. TCPDF will not resolve asset() URLs, and remote URLs require allow_url_fopen and add network latency to every render.

Use writeHTMLCell() with explicit dimensions when you need a boxed region (address blocks, side-by-side columns). TCPDF has no layout engine; the only reliable way to get two columns is two writeHTMLCell() calls with computed X coordinates via getX() / setX().

$y = $pdf->getY();
$pdf->writeHTMLCell(85, 0, 15,  $y, $billingHtml, 0, 1, false, true, 'L');
$pdf->writeHTMLCell(85, 0, 110, $y, $shippingHtml, 0, 1, false, true, 'L');
$pdf->setY($pdf->getY() + 5);

Repeat table headers across page breaks. TCPDF honors <thead> inside writeHTML():

<table border="1" cellpadding="4">
  <thead>
    <tr><th>Item</th><th>Qty</th><th align="right">Total</th></tr>
  </thead>
  <tbody> ... </tbody>
</table>

Without <thead>, long invoices get headerless continuation pages. This is the single most common visual defect in TCPDF invoices.

Persist with Storage, not the 'F' output mode. If you need a stored file, take the string and let Laravel handle the disk:

Storage::disk('s3')->put("invoices/{$invoice->id}.pdf", $pdf->Output('', 'S'));

Output($path, 'F') writes with raw fopen/fwrite, ignoring Laravel's filesystem abstraction, S3, and file locking. Reserve 'F' for cases where you genuinely need a local temp file (e.g. handing to a mailer library that requires a path).

Generate asynchronously for anything non-trivial. PDF rendering is CPU-bound and TCPDF is slow — a 200-line invoice table can take hundreds of milliseconds, a 5000-row report can take tens of seconds and eat memory. Queue it:

class GenerateInvoicePdf implements ShouldQueue
{
    public function handle(): void
    {
        $string = (new InvoicePdf(...))->render($this->invoice);
        Storage::put($this->path, $string);
        $this->invoice->update(['pdf_path' => $this->path]);
    }
}

Take advantage of TCPDF's differentiators over FPDF/mPDF-lite: native 1D/2D barcodes and digital signatures.

$pdf->write2DBarcode($invoice->number, 'QRCODE,H', 160, 12, 30, 30, [], 'N');
$pdf->write1DBarcode($invoice->number, 'C128', 15, 250, 60, 12, 0.4, [], 'N');

Testing: assert on the output magic bytes rather than the whole string.

$response = $this->actingAs($user)->get("/invoices/{$invoice->id}/pdf");
$response->assertOk();
$this->assertStringStartsWith('%PDF-', $response->getContent());

If you need deeper assertions, parse with smalot/pdfparser in tests only.

Gotchas and Tips

Class-name collision with tecnickcom/tcpdf. Both packages define the global TCPDF class. If any transitive dependency pulls in tecnickcom/tcpdf (many PDF-related packages do), you get a fatal "cannot redeclare class" or, worse, silent version mixing. Run composer why tecnickcom/tcpdf and composer show | grep -i tcpdf before committing to this package. The upstream tecnickcom/tcpdf is the actively maintained one; this fork has zero dependents and zero stars, so pin it exactly ("soqrate/tcpdf": "x.y.z") and be prepared to swap it out if it goes stale.

Path constants must be defined before the class loads. TCPDF checks for K_PATH_MAIN, K_PATH_FONTS, K_PATH_CACHE, K_PATH_IMAGES, and K_TCPDF_EXTERNAL_CONFIG at include time. If you need custom paths (e.g. cache into storage/framework/cache/tcpdf), define them in AppServiceProvider::register() before anything autoloads the class — or point K_TCPDF_EXTERNAL_CONFIG at your own config file that defines everything.

// AppServiceProvider::register(), runs early
if (!defined('K_PATH_CACHE')) {
    define('K_PATH_CACHE', storage_path('framework/cache/tcpdf'));
}

Error() kills the process. TCPDF's internal error handler writes a message and calls die(). Inside a queue worker or an HTTP request this is an uncatchable failure — no exception, no log, just a dead worker. Subclass and override it to throw:

public function Error($msg)
{
    throw new \RuntimeException('TCPDF: ' . $msg);
}

Font headaches. Only core fonts work without setup. Using any TTF requires addTTFfont(), which is slow, writes .php/.z files into the fonts directory, and must be run once — never at request time in production. The packaged fonts/ directory is often not writable in deployed apps (vendor/ is read-only in many setups). Either run font generation at build time and commit the output, or set K_PATH_FONTS to a writable storage/ path. addTTFfont() also has known issues with certain font variants and silently produces broken glyph widths.

Unicode: pass true as the fourth constructor arg and set dejavusans if your content has characters outside Latin-1 (Cyrillic, CJK, currency symbols, em-dashes). helvetica will render □ for anything unsupported — and it fails silently, so this ships to production undetected unless you eyeball a PDF with real customer data.

Cache directory must exist and be writable. TCPDF writes temporary files there. In Docker images with a non-root user this is a classic Permission denied at Output() time. Ensure storage/framework/cache/tcpdf exists with 0775 and is in your deploy script's directory-creation step.

The HTML/CSS support is a strict, ancient subset. No flexbox, no grid, no @media, no pseudo-classes, no CSS variables, no position. Supported roughly: font-family, font-size, font-weight, color, background-color, border, padding, margin, text-align, line-height, width/height on tables. <div> behaves unpredictably compared to <p>/<table>. Debugging trick: render the same Blade view in the browser first — if it looks like a table-based 2003 email template, TCPDF can probably handle it.

Page breaks ignore SetAutoPageBreak inside <table> rows in some versions. Force a break manually with $pdf->AddPage() between logical sections, or split large tables into multiple writeHTML() calls with a page check:

if ($pdf->getY() > 240) {
    $pdf->AddPage();
}

Use transactions for multi-part documents. If you build a 40-page report and page 38 throws, a partial PDF may already be in the output buffer:

$pdf->startTransaction();
try {
    // ... render ...
    $pdf->commitTransaction();
} catch (\Throwable $e) {
    $pdf->rollbackTransaction();
    throw $e;
}

Memory blows up with images. TCPDF decodes and holds images. A 4MB JPEG at 300 DPI in a loop over 100 line items will exhaust a 128M memory_limit. Downscale images before embedding, deduplicate identical images (TCPDF re-embeds the same file each Image() call unless you reuse it), and set ini_set('memory_limit', '512M') in the job if unavoidable.

Extension point: override Header()/Footer(), not the config file. Everything you'd want to customize per-document — logo, watermarking, footer text — belongs in a subclass. Editing config/tcpdf_config.php in vendor/ is lost on the next composer install and is invisible in code review.

Suppress the default header/footer when you want full control: setPrintHeader(false) and setPrintFooter(false) on the instance — but note SetAutoPageBreak's bottom margin should still account for the space a footer would have used, or content will collide with it on later pages.

Output() name argument and 'S' mode: $pdf->Output('', 'S') returns the raw bytes with no filename side effects. Prefer this over Output('doc.pdf', 'S') in a string-returning context to avoid TCPDF's internal filename munging on characters like / and :.

No Laravel integration means no testing helpers, no facades, no config. Wrap this package behind your own App\Pdf\* classes with a narrow interface (render(): string). That gives you a seam to swap in mpdf, dompdf, or a headless-Chrome renderer later — worth doing given this fork's zero-activity profile.

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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package