patressz/laravel-pdf

A Laravel package for generating PDF using playwright.

v0.6.0 2025-07-29 20:39 UTC

This package is auto-updated.

Last update: 2025-08-01 16:29:16 UTC


README

Tests

A modern PHP package for generating PDFs from HTML using Playwright in Laravel applications.

Features

  • πŸš€ Generate PDFs from Blade views or raw HTML
  • πŸ“„ Multiple PDF formats (A0-A6, Letter, Legal, Tabloid, Ledger) - defaults to A4
  • πŸ“ Custom dimensions with flexible units (mm, cm, in, px)
  • 🎨 Full CSS support with Playwright rendering
  • πŸ–ΌοΈ Print background graphics and images
  • πŸ“‘ Header and footer templates with dynamic content
  • πŸ”„ Landscape and portrait orientations
  • πŸ“ Configurable margins with multiple units
  • πŸ” Scale factor control (0.1-2.0)
  • πŸ“– Tagged (accessible) PDF generation
  • πŸ“„ Page range selection
  • πŸ’Ύ Save to file or return as base64/binary
  • πŸ“₯ Direct download response
  • πŸ”§ Configurable Node.js binary path
  • βœ‚οΈ Convenient Blade directives for page breaks and numbering
  • 🧩 Extensible with custom macros and conditional methods

Requirements

  • PHP 8.3+ or 8.4+
  • Laravel 11.0+ or 12.0+
  • Node.js 18+
  • Yarn or NPM

Installation

  1. Install the package via Composer:
composer require patressz/laravel-pdf
  1. Install Node.js dependencies:
yarn install
# or
npm install
  1. Install Playwright browser:
yarn playwright install chromium --with-deps
# or
npx playwright install chromium --with-deps

Usage

Basic PDF Generation

use Patressz\LaravelPdf\Facades\Pdf;

// Generate PDF from Blade view
$pdf = Pdf::view('invoice', ['user' => $user])
    ->format('A4')
    ->save('invoice.pdf');

// Generate PDF from raw HTML
$pdf = Pdf::html('<h1>Hello World</h1><p>This is a PDF.</p>')
    ->format('A4')
    ->save('document.pdf');

Generating PDF from URL

You can generate a PDF directly from a public URL (e.g., an external website):

use Patressz\LaravelPdf\Facades\Pdf;

// Generate PDF from a URL
$pdf = Pdf::fromUrl('https://example.com/invoice/123')
    ->format('A4')
    ->save('invoice.pdf');

// Download PDF generated from a URL
return Pdf::fromUrl('https://example.com/invoice/123')
    ->format('A4')
    ->download('invoice.pdf');

The URL must start with http:// or https://. If the URL is invalid, an InvalidArgumentException will be thrown.

Download PDF response

// Direct download
return Pdf::view('invoice', ['user' => $user])
    ->format('A4')
    ->download('invoice.pdf');

Inline PDF response

// Display in browser
return Pdf::view('invoice', ['user' => $user])
    ->format('A4')
    ->inline('invoice.pdf');

Base64 output

// Get PDF as base64 string
$base64 = Pdf::view('invoice', ['user' => $user])
    ->format('A4')
    ->base64();

Binary output

// Get PDF as binary string
$binaryPdf = Pdf::view('invoice', ['user' => $user])
    ->format('A4')
    ->raw();

Advanced configuration

Page dimensions and orientation

use Patressz\LaravelPdf\Enums\Format;
use Patressz\LaravelPdf\Enums\Unit;
use Patressz\LaravelPdf\Facades\Pdf;

// Set custom width and height
$pdf = Pdf::view('document')
    ->width(210, Unit::Millimeter)  // or ->width(210, 'mm')
    ->height(297, Unit::Millimeter) // or ->height(297, 'mm')
    ->save('document.pdf');

// Set landscape orientation
$pdf = Pdf::view('document')
    ->format(Format::A4)
    ->landscape()
    ->save('document.pdf');

Margins

// Set margins (top, right, bottom, left)
$pdf = Pdf::view('document')
    ->margins(20, 15, 20, 15, Unit::Millimeter)  // All sides
    ->save('document.pdf');

// Available units: mm, cm, in, px
$pdf = Pdf::view('document')
    ->margins(0.8, 0.6, 0.8, 0.6, Unit::Inch)
    ->save('document.pdf');

Headers and Footers

// Add header template
$pdf = Pdf::view('document')
    ->headerTemplate('<div style="font-size: 10px; text-align: center;">Page <span class="pageNumber"></span> of <span class="totalPages"></span></div>')
    ->save('document.pdf');

// Add footer template
$pdf = Pdf::view('document')
    ->footerTemplate('<div style="font-size: 8px; text-align: center;">Generated on <span class="date"></span></div>')
    ->save('document.pdf');

// Using Blade views for headers/footers
$pdf = Pdf::view('document')
    ->headerTemplate(view('pdf.header'))
    ->footerTemplate(view('pdf.footer'))
    ->save('document.pdf');

Blade directives

Laravel PDF provides convenient Blade directives for PDF generation:

// In your header/footer templates
{{-- Header template with page numbering --}}
<div class="page-header">
    Page @pageNumber of @totalPages
</div>

// In your main document content
{{-- Page break in main content --}}
<div class="section">
    First page content
</div>
@pageBreak
<div class="section">
    Second page content
</div>

header/footer template limitations:

  • Script tags inside templates are not evaluated (no JavaScript)
  • Page styles are not visible inside templates (use inline styles)
  • Only @pageNumber and @totalPages directives work in templates
  • Main content can use JavaScript and @pageBreak directive

Example usage in header/footer templates:

// resources/views/pdf/header.blade.php
<div style="text-align: center; font-size: 12px; border-bottom: 1px solid #ccc; padding-bottom: 10px;">
    <h3>{{ $company ?? 'Company Name' }}</h3>
    <p>Page @pageNumber of @totalPages</p>
</div>

// resources/views/pdf/footer.blade.php  
<div style="text-align: center; font-size: 10px; border-top: 1px solid #ccc; padding-top: 10px;">
    <p>Generated on {{ now()->format('Y-m-d H:i:s') }} | Page @pageNumber</p>
</div>

// Using the Pdf facade
Pdf::view('invoice', $data)
    ->headerTemplate(view('pdf.header', ['company' => 'ACME Corp']))
    ->footerTemplate(view('pdf.footer'))
    ->save('invoice.pdf');

Example usage in main document content:

{{-- resources/views/document.blade.php --}}
<!DOCTYPE html>
<html>
<head>
    <title>Multi-page Document</title>
    <script>
        // JavaScript works in main content
        document.addEventListener('DOMContentLoaded', function() {
            // Replace placeholder text with current date
            const placeholder = document.getElementById('current-date');
            if (placeholder) {
                placeholder.textContent = new Date().toLocaleDateString();
            }
        });
    </script>
</head>
<body>
    <div class="section">
        <h1>First Section</h1>
        <p>Generated on: <span id="current-date">[DATE_PLACEHOLDER]</span></p>
        <p>Content for the first page...</p>
    </div>
    
    {{-- Force page break using directive --}}
    @pageBreak
    
    <div class="section">
        <h1>Second Section</h1>
        <p>Content for the second page...</p>
    </div>
    
    @pageBreak
    
    <div class="section">
        <h1>Third Section</h1>
        <p>Content for the third page...</p>
    </div>
</body>
</html>

Print options

$pdf = Pdf::view('document')
    ->printBackground()        // Include background graphics
    ->scale(1.2)              // Scale factor (0.1 - 2.0)
    ->tagged()                // Generate tagged (accessible) PDF
    ->outline()               // Embed document outline
    ->preferCSSPageSize()     // Prefer CSS @page size over format
    ->pageRanges('1-5,8,11-13') // Print specific page ranges
    ->save('document.pdf');

Available formats

The default format is A4. You can specify format using either string or enum:

Available formats:

  • A0, A1, A2, A3, A4, A5, A6
  • Letter, Legal, Tabloid, Ledger

Available units for dimensions and margins:

  • mm (millimeters) - default
  • cm (centimeters)
  • in (inches)
  • px (pixels)
use Patressz\LaravelPdf\Enums\Format;
use Patressz\LaravelPdf\Enums\Unit;
use Patressz\LaravelPdf\Facades\Pdf;

// Using enum (recommended)
$pdf = Pdf::view('document')
    ->format(Format::A4)
    ->save('document.pdf');

// Using string
$pdf = Pdf::view('document')
    ->format('A4')
    ->save('document.pdf');

// Default format (A4) - no need to specify
$pdf = Pdf::view('document')
    ->save('document.pdf');

Configuration

Download filename

// Set custom filename for downloads
$pdf = Pdf::view('document')
    ->name('my-custom-document.pdf')  // .pdf extension is optional
    ->download();

// Or specify filename in download method
$pdf = Pdf::view('document')
    ->download('invoice-2023.pdf');

Custom Node.js binary path

$pdf = Pdf::setNodeBinaryPath('/custom/path/to/node')
    ->view('document')
    ->save('document.pdf');

Custom response headers

$pdf = Pdf::view('document')
    ->addHeaders([
        'Cache-Control' => 'no-cache',
        'X-Custom-Header' => 'value'
    ])
    ->save('document.pdf');

Examples

Invoice generation

// resources/views/invoice.blade.php
<!DOCTYPE html>
<html>
<head>
    <title>Invoice #{{ $invoice->number }}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .header { text-align: center; margin-bottom: 30px; }
        .invoice-details { margin: 20px 0; }
        .items { margin: 20px 0; }
        .item { padding: 5px 0; border-bottom: 1px solid #eee; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Invoice #{{ $invoice->number }}</h1>
    </div>
    
    <div class="invoice-details">
        <p><strong>Date:</strong> {{ $invoice->date }}</p>
        <p><strong>Customer:</strong> {{ $invoice->customer->name }}</p>
    </div>
    
    <div class="items">
        <h3>Invoice Items</h3>
        @foreach($invoice->items as $item)
            <div class="item">
                {{ $item->description }} - ${{ $item->amount }}
            </div>
        @endforeach
    </div>
    
    {{-- Use @pageBreak directive for page breaks --}}
    @if($invoice->hasAdditionalPages)
        @pageBreak
        <div class="additional-content">
            {{-- Additional invoice content --}}
        </div>
    @endif
</body>
</html>

{{-- resources/views/pdf/invoice-header.blade.php --}}
<div style="text-align: center; font-size: 12px; border-bottom: 1px solid #ccc; padding: 10px;">
    <strong>{{ $company ?? 'Your Company' }}</strong><br>
    Invoice #{{ $invoice->number }} | Page @pageNumber of @totalPages
</div>

{{-- resources/views/pdf/invoice-footer.blade.php --}}
<div style="text-align: center; font-size: 10px; border-top: 1px solid #ccc; padding: 10px;">
    Generated on {{ now()->format('Y-m-d H:i:s') }} | Page @pageNumber of @totalPages
</div>
// Controller
public function downloadInvoice(Invoice $invoice)
{
    return Pdf::view('invoice', compact('invoice'))
        ->format('A4')
        ->margins(20, 15, 20, 15) // 20mm top/bottom, 15mm left/right
        ->printBackground()       // Include background colors/images
        ->headerTemplate(view('pdf.invoice-header', compact('invoice')))
        ->footerTemplate(view('pdf.invoice-footer', compact('invoice')))
        ->download("invoice-{$invoice->number}.pdf");
}

public function generateReport()
{
    return Pdf::view('reports.monthly')
        ->format('A4')
        ->landscape()
        ->margins(15, 10, 15, 10)
        ->scale(0.8)  // Reduce scale to fit more content
        ->save(storage_path('reports/monthly-report.pdf'));
}

Method chaining

All configuration methods return the PdfBuilder instance, allowing for fluent method chaining through the Pdf facade:

use Patressz\LaravelPdf\Facades\Pdf;

return Pdf::view('complex-document', $data)
    ->format(Format::A4)
    ->landscape()
    ->margins(25, 20, 25, 20, Unit::Millimeter)
    ->printBackground()
    ->scale(1.1)
    ->headerTemplate('<div style="text-align: center; font-size: 12px;">Company Header</div>')
    ->footerTemplate('<div style="text-align: center; font-size: 10px;">Page <span class="pageNumber"></span></div>')
    ->name('complex-document.pdf')
    ->download();

Conditional methods

The underlying PdfBuilder class includes Laravel's Conditionable trait, which you can access through the Pdf facade to conditionally apply methods:

$pdf = Pdf::view('document', $data)
    ->when($user->isPremium(), function ($pdf) {
        return $pdf->headerTemplate(view('pdf.premium-header'));
    })
    ->when($includeFooter, fn($pdf) => $pdf->footerTemplate(view('pdf.footer')))
    ->unless($isPreview, fn($pdf) => $pdf->printBackground())
    ->format(Format::A4)
    ->save('document.pdf');

Available conditional methods:

  • when($condition, $callback, $default = null) - Execute callback when condition is true
  • unless($condition, $callback, $default = null) - Execute callback when condition is false

Custom macros

The PdfBuilder class includes Laravel's Macroable trait, allowing you to extend its functionality with custom methods. Both the real PdfBuilder and the FakePdfBuilder (used in testing) support macros:

use Patressz\LaravelPdf\Facades\Pdf;

// Define a custom macro in a service provider using the facade
Pdf::macro('invoice', function (Invoice $invoice) {
    return $this->view('pdf.invoice', compact('invoice'))
        ->format('A4')
        ->margins(20, 15, 20, 15)
        ->headerTemplate(view('pdf.invoice-header', compact('invoice')))
        ->footerTemplate(view('pdf.invoice-footer'));
});

// Use the macro
$pdf = Pdf::invoice($invoice)->download("invoice-{$invoice->number}.pdf");

Note: Macros can be defined using either Pdf::macro() facade method or directly on the PdfBuilder class. Both approaches make the macros available through the Pdf facade and work seamlessly with both production code and testing (when using Pdf::fake()).

Troubleshooting

Node.js not found

If you encounter "Node.js binary not found" errors:

// Set custom Node.js path
$pdf = Pdf::setNodeBinaryPath('/usr/local/bin/node')  // or your custom path
    ->view('document')
    ->save('document.pdf');

Memory issues

For large documents, consider:

  • Using pageRanges() to process documents in chunks
  • Reducing image sizes in your HTML
  • Optimizing CSS and avoiding complex layouts

CSS issues

  • Ensure all CSS is inline or included in <style> tags
  • Use absolute paths for images and assets
  • Test with printBackground() if backgrounds aren't showing

JavaScript and Header/Footer limitations

JavaScript support:

  • βœ… JavaScript works in main document content
  • ❌ JavaScript is not executed in headerTemplate() and footerTemplate()
  • ❌ Page styles are not visible in header/footer templates (use inline styles only)
// βœ… Works - JavaScript in main content
Pdf::html('
    <script>
        document.addEventListener("DOMContentLoaded", function() {
            document.getElementById("status").textContent = "Generated successfully";
        });
    </script>
    <h1>Document</h1>
    <p>Status: <span id="status">Loading...</span></p>
')
->save('document.pdf');

// ❌ Won't work - JavaScript in header template
Pdf::view('document')
    ->headerTemplate('<script>document.write("This won\'t work");</script><div>Header</div>')
    ->save('document.pdf');

API Reference

Content methods

Method Parameters Description
view() string $view, array $data = [] Render Blade view as PDF content
html() string $html Set raw HTML content (takes precedence over view)
fromUrl() string $url Generate PDF from a public URL (must start with http:// or https://)

Template methods

Method Parameters Description
headerTemplate() string|View $template Set header template with special classes
footerTemplate() string|View $template Set footer template with special classes

Format & dimensions

Method Parameters Description
format() string|Format $format Set paper format (A0-A6, Letter, Legal, etc.)
width() float $width, Unit|string $unit = 'mm' Set custom width
height() float $height, Unit|string $unit = 'mm' Set custom height
landscape() - Set landscape orientation
margins() float $top, float $right, float $bottom, float $left, Unit|string $unit = 'mm' Set page margins

Print options

Method Parameters Description
printBackground() - Include background graphics and colors
displayHeaderFooter() - Enable header/footer display (auto-enabled by templates)
scale() float $scale Set scale factor (0.1 - 2.0)
tagged() - Generate tagged (accessible) PDF
outline() - Embed document outline
preferCSSPageSize() - Prefer CSS @page size over format options
pageRanges() string $ranges Print specific pages (e.g., '1-5,8,11-13')

Output methods

Method Parameters Description
save() string $outputPath Save PDF to file and return path
download() string $filename = 'document.pdf' Set download headers for attachment
inline() string $filename = 'document.pdf' Set inline headers for browser display
base64() - Return PDF as base64 string
raw() - Return PDF as binary string
toResponse() $request Return as HTTP response (Responsable interface)

Configuration methods

Method Parameters Description
name() string $filename Set download filename
setNodeBinaryPath() string $path Set custom Node.js binary path
addHeaders() array $headers Add custom response headers

Extension methods

Method Parameters Description
when() mixed $condition, callable $callback, callable|null $default Execute callback when condition is true
unless() mixed $condition, callable $callback, callable|null $default Execute callback when condition is false
macro() string $name, callable $macro Static method - Register a custom macro
mixin() object $mixin, bool $replace = true Static method - Mix another object into the class
hasMacro() string $name Static method - Check if macro exists
flushMacros() - Static method - Remove all macros

Blade directives

Directive Output Description
@pageNumber <span class="pageNumber"></span> Current page number (header/footer only)
@totalPages <span class="totalPages"></span> Total pages count (header/footer only)
@pageBreak <div style="page-break-after: always;"></div> Force page break (main content only)

Note: @pageNumber and @totalPages only work in headerTemplate() and footerTemplate() views. @pageBreak only works in main document content. Header/footer templates have limitations: no JavaScript execution and no access to page styles.

Testing PDF generation in your application

Laravel PDF provides a fake implementation for testing without actually generating PDFs:

use Patressz\LaravelPdf\Facades\Pdf;
use Illuminate\View\View;

it('can download invoice', function () {
    // Fake the PDF generation
    Pdf::fake();
    
    $invoice = Invoice::factory()->create();
    
    $response = $this->get("/invoices/{$invoice->id}/download");
    
    $response->assertSuccessful();
    
    // Assert PDF was generated with correct view and data
    Pdf::assertView('invoice', function (View $view, array $data) use ($invoice): bool {
        return $data['invoice']->id === $invoice->id;
    });
    
    // Assert PDF configuration
    Pdf::assertFormat('A4');
    Pdf::assertMargins(20, 15, 20, 15, 'mm');
    Pdf::assertPrintBackground();
    Pdf::assertDownloaded();
});

Available test assertions

The fake PDF implementation provides comprehensive assertions:

// Content assertions
Pdf::assertUrl('https://www.example.com');
Pdf::assertView('invoice'); // Assert specific view was used
Pdf::assertView('invoice', function (View $view, array $data): bool {
    // Callback receives View object and data array
    return $data['invoice']->total > 100;
});
Pdf::assertHtml('<h1>Test</h1>'); // Assert raw HTML content
Pdf::assertHeaderTemplate($expectedHtml); // Assert header template
Pdf::assertFooterTemplate($expectedHtml); // Assert footer template

// Format and dimensions
Pdf::assertFormat('A4'); // or Format::A4
Pdf::assertWidth(210, 'mm');
Pdf::assertHeight(297, 'mm');
Pdf::assertLandscape(); // Assert landscape is enabled

// Options
Pdf::assertMargins(20, 15, 20, 15, 'mm');
Pdf::assertPrintBackground();
Pdf::assertDisplayHeaderFooter();
Pdf::assertScale(1.2);
Pdf::assertTagged();
Pdf::assertOutline();
Pdf::assertPreferCSSPageSize();
Pdf::assertPageRanges('1-5,8');

// Output assertions
Pdf::assertName('invoice.pdf'); // Assert download filename
Pdf::assertSaved('/path/to/file.pdf'); // Assert file was saved
Pdf::assertDownloaded('invoice.pdf'); // Assert download headers set
Pdf::assertInline('invoice.pdf'); // Assert inline headers set

Testing with callbacks

Use callbacks for more complex assertions:

use Illuminate\View\View;

beforeEach(function () {
    Pdf::fake();
});

it('validates invoice data in PDF', function () {
    $invoice = Invoice::factory()->create(['total' => 150]);
    
    Pdf::view('invoice', compact('invoice'));
    
    // Test view and its data
    Pdf::assertView('invoice', function (View $view, array $data) use ($invoice): bool {
        return $data['invoice']->total > 100 
            && $data['invoice']->id === $invoice->id
            && $view->name() === 'invoice';
    });
});

it('asserts simple view without callback', function () {
    Pdf::view('invoice');
    
    Pdf::assertView('invoice');
});

Testing services that use PDF

When testing components that use PDF generation:

use Illuminate\View\View;

it('generates monthly report', function () {
    Pdf::fake();
    
    $service = new ReportService();
    $service->generateMonthlyReport(2025, 1);
    
    Pdf::assertView('reports.monthly', function (View $view, array $data): bool {
        return $data['month'] === 1 && $data['year'] === 2025;
    });
    Pdf::assertFormat('A4');
    Pdf::assertLandscape(true);
});

Test example with all features

Complete test example showing multiple PDF features:

it('generates complex PDF with multiple features', function () {
    Pdf::fake();
    
    $invoice = Invoice::factory()->create();
    
    Pdf::view('invoice', compact('invoice'))
        ->format(Format::A4)
        ->landscape()
        ->margins(20, 15, 20, 15, Unit::Millimeter)
        ->printBackground()
        ->scale(1.1)
        ->headerTemplate('<div>Header</div>')
        ->footerTemplate('<div>Footer</div>')
        ->name('test-invoice.pdf')
        ->download();
        
    // Assert all configurations
    Pdf::assertView('invoice');
    Pdf::assertFormat('A4');
    Pdf::assertLandscape();
    Pdf::assertMargins(20, 15, 20, 15, 'mm');
    Pdf::assertPrintBackground();
    Pdf::assertScale(1.1);
    Pdf::assertHeaderTemplate('<div>Header</div>');
    Pdf::assertFooterTemplate('<div>Footer</div>');
    Pdf::assertName('test-invoice.pdf');
    Pdf::assertDownloaded('test-invoice.pdf');
});

Contributing

Please see CONTRIBUTING for details.

Credits

License

The MIT License (MIT). Please see License File for more information.