PDF Generation Guide
Last Updated: 2026-01-13 Status: Implemented Plan Reference: 009-pdf-invoice-generation.md, 050-pdf-generation-enhancement.md, 086-pdf-generation-improvement.md
Overview
The PDF Generation system creates professional, downloadable PDF documents for invoices and custom documents. It uses the DomPDF library to render Blade templates as PDF files, with support for custom fonts, advanced layout options, reusable components, and a visual template editor.
Table of Contents
- Accessing PDFs
- Features
- How to Use
- PDF Templates
- Async PDF Generation
- Bulk PDF Generation
- Custom Fonts
- PDF Components
- Visual Template Editor
- Layout Options
- Company Configuration
- Technical Architecture
- Related Features
Accessing PDFs
Navigation
| Access Point | URL | Role |
|---|---|---|
| View Invoice PDF | /admin/invoices/{id}/pdf | Admin |
| Download Invoice PDF | /admin/invoices/{id}/pdf/download | Admin |
| Portal View PDF | /portal/invoices/{id}/pdf | Client User |
| Portal Download PDF | /portal/invoices/{id}/pdf/download | Client User |
| Manage PDF Fonts | /admin/pdf-fonts | Admin |
| Manage PDF Components | /admin/pdf-components | Admin |
| Template Editor | /admin/documents/templates/{id}/editor | Admin |
Permissions
| Action | Admin | Client User |
|---|---|---|
| View any invoice PDF | ✅ | ❌ |
| View own invoice PDF | ✅ | ✅ |
| Download any invoice PDF | ✅ | ❌ |
| Download own invoice PDF | ✅ | ✅ |
| Manage PDF fonts | ✅ | ❌ |
| Manage PDF components | ✅ | ❌ |
| Use template editor | ✅ | ❌ |
Features
Invoice PDF Content
- Company header with logo
- Invoice number and status badge
- Bill-to client information
- Issue date and due date
- Line items table with totals
- Notes section
- "PAID" watermark for paid invoices
- Professional footer
PDF Options
- View in browser
- Download as file
- Filename includes invoice number
- Multiple paper sizes (Letter, Legal, A4, A5, A3)
- Portrait or landscape orientation
- Custom margins
Enhanced PDF Features (Plan 050)
- Custom Fonts: Upload and manage TTF, OTF, WOFF fonts
- Reusable Components: Headers, footers, signatures, letterheads
- Visual Template Editor: WYSIWYG editing with live preview
- Advanced Layout: Configurable margins, headers, footers
- Template Variables: Dynamic content placeholders
How to Use
Viewing a PDF in Browser
- Navigate to any invoice detail page
- Click "View PDF" button
- PDF opens in browser tab
- Use browser controls to print or save
Downloading a PDF
- Navigate to any invoice detail page
- Click "Download PDF" button
- PDF downloads with filename:
invoice-{number}.pdf - Open with any PDF reader
PDF from Invoice List
- Navigate to Admin → Invoices
- Click the PDF icon in the actions column
- PDF downloads directly
PDF Templates
PDF Templates provide reusable, versioned templates for generating consistent PDFs.
Managing Templates
Navigate to Admin → PDF Templates to manage templates.
| Action | Description |
|---|---|
| Create Template | Create new PDF template |
| Edit Template | Modify content, header, footer |
| Preview | View PDF with sample data |
| Restore Version | Restore previous version |
| Toggle Active | Enable/disable template |
Creating a Template
- Navigate to Admin → PDF Templates → Create
- Fill in the details:
- Name: Template display name
- Type: invoice, report, or statement
- Content: HTML template with variables
- Header: Optional page header
- Footer: Optional page footer
- Click Create Template
Template Variables
Use Blade-style placeholders in template content:
<div class="invoice">
<h1>Invoice {{ invoice_number }}</h1>
<p>Client: {{ client_name }}</p>
<p>Total: {{ total }}</p>
</div>
Invoice Variables:
{{ invoice_number }}- Invoice number{{ client_name }}- Client name{{ client_email }}- Client email{{ date }}- Issue date{{ due_date }}- Due date{{ subtotal }}- Subtotal amount{{ tax }}- Tax amount{{ total }}- Total amount
Report Variables:
{{ title }}- Report title{{ date_range }}- Date range{{ summary }}- Summary text{{ generated_at }}- Generation timestamp
Template Versioning
Templates are automatically versioned when updated:
- Edit a template
- Click Save Changes
- Previous version is automatically saved
- View version history in sidebar
- Click Restore to revert to previous version
Routes
GET /admin/pdf-templates # List templates
POST /admin/pdf-templates # Create template
GET /admin/pdf-templates/{id}/edit # Edit form
PUT /admin/pdf-templates/{id} # Update template
GET /admin/pdf-templates/{id}/preview # Preview PDF
POST /admin/pdf-templates/{id}/restore/{ver} # Restore version
Async PDF Generation
Large PDFs are generated asynchronously using queued jobs.
How It Works
- User requests PDF generation
PdfGenerationrecord created with status "pending"GeneratePdfJobdispatched to queue- Job processes and generates PDF
- User notified when complete
- PDF available for download
Generation Status
| Status | Description |
|---|---|
pending | Job queued, waiting to process |
processing | Job currently running |
completed | PDF ready for download |
failed | Generation failed with error |
Viewing Generations
Navigate to Admin → PDF Templates → My Generations to view your PDF generations.
Receiving Notifications
When a PDF is ready:
- Email notification sent with download link
- Database notification shown in app
- Download link valid for 1 hour
API Usage
use App\Models\PdfGeneration;
use App\Jobs\GeneratePdfJob;
// Create generation request
$generation = PdfGeneration::create([
'user_id' => auth()->id(),
'template_id' => $template->id,
'type' => 'invoice',
'data' => ['invoice_number' => 'INV-001', ...],
'status' => PdfGeneration::STATUS_PENDING,
]);
// Dispatch job
GeneratePdfJob::dispatch($generation);
// Check status later
if ($generation->isComplete()) {
$url = $generation->getDownloadUrl();
}
Bulk PDF Generation
Generate multiple PDFs at once for batch processing.
Generating Bulk PDFs
use App\Services\Pdf\BulkPdfService;
use App\Models\Invoice;
$bulkService = app(BulkPdfService::class);
$template = PdfTemplate::ofType('invoice')->active()->first();
$invoices = Invoice::whereIn('id', $selectedIds)->get();
// Generate PDFs asynchronously
$generations = $bulkService->generateBulk($template, $invoices);
// Get generation IDs for status tracking
$ids = collect($generations)->pluck('id')->toArray();
Checking Bulk Status
$status = $bulkService->getBulkStatus($ids);
// Returns:
// [
// 'total' => 10,
// 'pending' => 2,
// 'processing' => 1,
// 'completed' => 6,
// 'failed' => 1,
// 'is_complete' => false,
// ]
Downloading as ZIP
When all PDFs are complete, download as ZIP:
$zipPath = $bulkService->generateZip($ids);
return response()->download($zipPath, 'documents.zip')
->deleteFileAfterSend();
API Endpoints
# Check bulk status
GET /admin/pdf/bulk-status?ids[]=1&ids[]=2&ids[]=3
# Download as ZIP
GET /admin/pdf/bulk-download?ids[]=1&ids[]=2&ids[]=3
Custom Fonts
Managing Fonts
Navigate to Admin → PDF Fonts to manage custom fonts.
| Action | Description |
|---|---|
| Upload Font | Upload TTF, OTF, or WOFF font files |
| Toggle Active | Enable/disable fonts for use in templates |
| Preview Font | View font rendering at different sizes |
| Delete Font | Remove unused fonts |
Uploading a Font
- Navigate to Admin → PDF Fonts → Add Font
- Fill in the font details:
- Name: Display name for the font
- Family: CSS font-family name
- Weight: normal, bold, 300, 500, 600, 700
- Style: normal or italic
- Upload the font file (TTF, OTF, or WOFF)
- Click Create Font
Font Configuration
Location: config/pdf.php
'fonts' => [
'path' => storage_path('app/fonts'),
'allowed_types' => ['ttf', 'otf', 'woff'],
'max_size' => 5 * 1024 * 1024, // 5MB
],
Using Custom Fonts in Templates
Custom fonts are automatically available in the template editor. The FontManager service generates the necessary @font-face CSS declarations.
// Get available fonts
$fontManager = app(FontManager::class);
$fonts = $fontManager->getAvailableFonts();
// Get font embed styles
$fontStyles = $fontManager->getEmbedStyles();
PDF Components
What are PDF Components?
Reusable content blocks that can be inserted into document templates:
| Type | Description |
|---|---|
header | Document headers with company branding |
footer | Page footers with page numbers, copyright |
signature | Signature blocks with name/date placeholders |
letterhead | Full letterhead designs |
table | Styled table templates |
terms | Terms and conditions blocks |
Managing Components
Navigate to Admin → PDF Components to manage components.
| Action | Description |
|---|---|
| Create Component | Create new reusable component |
| Edit Component | Modify component content and styles |
| Duplicate | Copy an existing component |
| Toggle Global | Make component available to all users |
| Preview | View rendered component |
Creating a Component
- Navigate to Admin → PDF Components → Create
- Fill in the details:
- Name: Display name
- Type: header, footer, signature, etc.
- Category: branding, legal, financial, general
- Content: HTML content with variable placeholders
- Styles: Optional JSON styles configuration
- Variables: Define available placeholders
- Click Create Component
Component Variables
Use Blade-style placeholders in component content:
<div class="signature-block">
<div class="signature-line"></div>
<div class="signature-name">{{signer.name}}</div>
<div class="signature-date">{{date.today}}</div>
</div>
Visual Template Editor
Accessing the Editor
- Navigate to Admin → Document Templates
- Click on a template to view details
- Click "Open Editor" button
Editor Features
| Section | Description |
|---|---|
| Content Editor | Monaco-based code editor for template HTML |
| Layout Settings | Page size, orientation, margins |
| Typography | Font family, body/heading sizes |
| Colors | Primary, secondary, accent, background colors |
| Header Config | Left/center/right zones, height, border |
| Footer Config | Left/center/right zones, page numbers, border |
| Live Preview | Real-time PDF preview (iframe) |
Layout Options
| Option | Values |
|---|---|
| Page Size | Letter, Legal, A4, A5, A3 |
| Orientation | Portrait, Landscape |
| Margins | Top, Right, Bottom, Left (mm) |
Header/Footer Variables
| Variable | Description |
|---|---|
{{company.name}} | Company name |
{{company.logo}} | Company logo (HTML img tag) |
{{document.title}} | Document title |
{{document.number}} | Document number |
{{date.today}} | Current date |
{{page}} | Current page number |
{{pages}} | Total page count |
Layout Options
Page Sizes
| Size | Dimensions |
|---|---|
| Letter | 8.5" x 11" (612 x 792 pt) |
| Legal | 8.5" x 14" (612 x 1008 pt) |
| A4 | 210mm x 297mm (595.28 x 841.89 pt) |
| A5 | 148mm x 210mm (419.53 x 595.28 pt) |
| A3 | 297mm x 420mm (841.89 x 1190.55 pt) |
Margin Units
Margins can be specified in various units:
| Unit | Example |
|---|---|
| mm (default) | 25 or 25mm |
| inches | 1in |
| centimeters | 2.5cm |
| points | 72pt |
Default Layout
[
'page_size' => 'letter',
'orientation' => 'portrait',
'margin_top' => 25,
'margin_right' => 20,
'margin_bottom' => 25,
'margin_left' => 20,
]
Company Configuration
Environment Variables
Configure your company details in .env:
COMPANY_NAME="Your Company Name"
COMPANY_ADDRESS="123 Business Street"
COMPANY_CITY="Your City"
COMPANY_STATE="Your State"
COMPANY_ZIP="12345"
COMPANY_PHONE="+1 (555) 123-4567"
COMPANY_EMAIL="billing@yourcompany.com"
COMPANY_WEBSITE="https://yourcompany.com"
COMPANY_TAX_ID="XX-XXXXXXX"
COMPANY_LOGO="images/logo.png"
Configuration File
Location: config/company.php
return [
'name' => env('COMPANY_NAME', 'Your Company'),
'address' => env('COMPANY_ADDRESS', ''),
'city' => env('COMPANY_CITY', ''),
'state' => env('COMPANY_STATE', ''),
'zip' => env('COMPANY_ZIP', ''),
'phone' => env('COMPANY_PHONE', ''),
'email' => env('COMPANY_EMAIL', ''),
'website' => env('COMPANY_WEBSITE', ''),
'tax_id' => env('COMPANY_TAX_ID', ''),
'logo' => env('COMPANY_LOGO', ''),
];
PDF Configuration
Location: config/pdf.php
return [
'driver' => env('PDF_DRIVER', 'dompdf'),
'fonts' => [
'path' => storage_path('app/fonts'),
'allowed_types' => ['ttf', 'otf', 'woff'],
'max_size' => 5 * 1024 * 1024,
],
'defaults' => [
'page_size' => 'letter',
'orientation' => 'portrait',
'dpi' => 150,
'margins' => [
'top' => 25,
'right' => 20,
'bottom' => 25,
'left' => 20,
],
],
'colors' => [
'primary' => '#3B82F6',
'secondary' => '#1E40AF',
],
];
Technical Architecture
Package
DomPDF: barryvdh/laravel-dompdf
composer require barryvdh/laravel-dompdf
Database Schema
pdf_fonts Table
Schema::create('pdf_fonts', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('family');
$table->string('weight')->default('normal');
$table->string('style')->default('normal');
$table->string('file_path');
$table->integer('file_size')->default(0);
$table->boolean('is_active')->default(true);
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
$table->unique(['family', 'weight', 'style']);
});
pdf_components Table
Schema::create('pdf_components', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('type');
$table->string('category')->nullable();
$table->longText('content');
$table->json('styles')->nullable();
$table->json('variables')->nullable();
$table->text('description')->nullable();
$table->boolean('is_global')->default(false);
$table->foreignId('created_by')->constrained('users');
$table->timestamps();
});
document_templates Enhancements
// Added columns
$table->json('layout_options')->nullable();
$table->json('font_config')->nullable();
$table->json('header_config')->nullable();
$table->json('footer_config')->nullable();
$table->integer('version')->default(1);
Models
PdfFont Model
Location: app/Models/PdfFont.php
class PdfFont extends Model
{
protected $fillable = [
'name', 'family', 'weight', 'style',
'file_path', 'file_size', 'is_active', 'created_by',
];
protected $casts = [
'is_active' => 'boolean',
'file_size' => 'integer',
];
// Scopes
public function scopeActive($query) { ... }
// Accessors
public function getFullPathAttribute(): string { ... }
public function getFontFaceDeclarationAttribute(): string { ... }
public function getCssFontWeight(): string { ... }
}
PdfComponent Model
Location: app/Models/PdfComponent.php
class PdfComponent extends Model
{
protected $fillable = [
'name', 'slug', 'type', 'category', 'content',
'styles', 'variables', 'description', 'is_global', 'created_by',
];
protected $casts = [
'styles' => 'array',
'variables' => 'array',
'is_global' => 'boolean',
];
// Scopes
public function scopeGlobal($query) { ... }
public function scopeOfType($query, string $type) { ... }
}
Services
FontManager
Location: app/Services/Pdf/FontManager.php
class FontManager
{
public function registerFont(string $family, string $path, string $weight, string $style): void;
public function getEmbedStyles(): string;
public function uploadFont(UploadedFile $file, array $metadata): PdfFont;
public function validateFontFile(UploadedFile $file): void;
public function getAvailableFonts(): array;
public function getFontFamilies(): array;
public function getFontPath(string $family, string $weight, string $style): ?string;
public function deleteFont(PdfFont $font): bool;
public function registerFontsWithDompdf(Dompdf $dompdf): void;
}
LayoutManager
Location: app/Services/Pdf/LayoutManager.php
class LayoutManager
{
public function apply(array $config): array;
public function getPageSizes(): array;
public function getDefaultLayout(): array;
public function buildMarginCSS(array $margins): string;
public function renderHeader(array $config, array $variables = []): string;
public function renderFooter(array $config, array $variables = []): string;
public function getPageDimensions(string $size, string $orientation): array;
public function calculateContentHeight(array $layout, array $header, array $footer): float;
public function getHeaderVariables(): array;
public function getFooterVariables(): array;
}
StyleCompiler
Location: app/Services/Pdf/StyleCompiler.php
class StyleCompiler
{
public function compile(array $config): string;
public function generateTypographyCSS(array $fontConfig): string;
public function generateColorSchemeCSS(array $colors): string;
public function generateComponentCSS(array $components): string;
}
PdfGeneratorService
Location: app/Services/Documents/PdfGeneratorService.php
class PdfGeneratorService
{
public function __construct(
protected FontManager $fontManager,
protected LayoutManager $layoutManager,
protected StyleCompiler $styleCompiler
);
public function generate(GeneratedDocument $document, array $options = []): string;
public function stream(GeneratedDocument $document, array $options = []): mixed;
public function download(GeneratedDocument $document, array $options = []): mixed;
public function generatePreview(string $html, array $options = []): string;
public function generatePreviewHtml($template, array $variables = []): string;
public function generateFromHtml(string $html, array $options = []): string;
public function generateWithSignatures(GeneratedDocument $document): string;
}
Controllers
PdfFontController
Location: app/Http/Controllers/Admin/PdfFontController.php
class PdfFontController extends Controller
{
public function index(); // List all fonts with filtering
public function create(); // Show upload form
public function store(); // Upload and create font
public function show(); // View font details
public function edit(); // Edit font form
public function update(); // Update font metadata
public function destroy(); // Delete font
public function preview(); // Generate font preview PDF
public function toggleActive(); // Enable/disable font
}
PdfComponentController
Location: app/Http/Controllers/Admin/PdfComponentController.php
class PdfComponentController extends Controller
{
public function index(); // List all components
public function create(); // Show create form
public function store(); // Create component
public function show(); // View component
public function edit(); // Edit form
public function update(); // Update component
public function destroy(); // Delete component
public function duplicate(); // Clone component
public function toggleGlobal(); // Toggle global visibility
public function preview(); // Preview rendered component
}
Routes
// PDF Fonts
Route::resource('pdf-fonts', PdfFontController::class);
Route::get('pdf-fonts/{pdfFont}/preview', [PdfFontController::class, 'preview'])
->name('admin.pdf-fonts.preview');
Route::post('pdf-fonts/{pdfFont}/toggle-active', [PdfFontController::class, 'toggleActive'])
->name('admin.pdf-fonts.toggle-active');
// PDF Components
Route::resource('pdf-components', PdfComponentController::class);
Route::post('pdf-components/{pdfComponent}/duplicate', [PdfComponentController::class, 'duplicate'])
->name('admin.pdf-components.duplicate');
Route::post('pdf-components/{pdfComponent}/toggle-global', [PdfComponentController::class, 'toggleGlobal'])
->name('admin.pdf-components.toggle-global');
Route::post('pdf-components/preview', [PdfComponentController::class, 'preview'])
->name('admin.pdf-components.preview');
// Template Editor
Route::get('documents/templates/{template}/editor', [DocumentTemplateController::class, 'editor'])
->name('admin.documents.templates.editor');
Route::post('documents/templates/live-preview', [DocumentTemplateController::class, 'livePreview'])
->name('admin.documents.templates.live-preview');
Route::put('documents/templates/{template}/layout', [DocumentTemplateController::class, 'saveLayout'])
->name('admin.documents.templates.save-layout');
Views
| View | Description |
|---|---|
admin/pdf-fonts/index.blade.php | Font listing with filters |
admin/pdf-fonts/create.blade.php | Font upload form |
admin/pdf-fonts/edit.blade.php | Font edit form |
admin/pdf-fonts/show.blade.php | Font details and preview |
admin/pdf-components/index.blade.php | Component listing |
admin/pdf-components/create.blade.php | Component creation form |
admin/pdf-components/edit.blade.php | Component edit form |
admin/pdf-components/show.blade.php | Component preview |
admin/documents/templates/editor.blade.php | Visual template editor |
documents/pdf-layout-enhanced.blade.php | Enhanced PDF layout template |
Enhanced PDF Layout Template
Location: resources/views/documents/pdf-layout-enhanced.blade.php
Features:
- Custom font embedding via
@font-face - CSS variables for colors and typography
- Configurable header and footer zones
- Page number support
- Responsive content area
PDF Styling Tips
Fonts
- Upload custom fonts via the Font Manager
- Use
DejaVu Sansfor UTF-8 support as fallback - Keep font sizes 10-12pt for body text
- Use bold/italic variants for emphasis
Images
- Use absolute paths:
public_path('images/logo.png') - Keep images small for faster generation
- PNG works best for logos
- Maximum recommended logo size: 200x60 pixels
CSS Limitations
- Limited CSS3 support in DomPDF
- No flexbox or grid layout
- Use tables for complex layouts
- Inline styles are most reliable
Page Breaks
.page-break {
page-break-after: always;
}
.avoid-break {
page-break-inside: avoid;
}
Related Features
Dependencies
| Feature | Relationship |
|---|---|
| Invoicing | Provides invoice data |
| Client Management | Client bill-to information |
| Document Templates | Template configuration |
Complementary Features
| Feature | Description |
|---|---|
| Document Templates | Custom document generation |
| Reports | Report PDF exports |
| Notifications | Email PDF attachments |
| Digital Signatures | Signed document PDFs |
Best Practices
For Administrators
- Configure company details before generating invoices
- Upload brand fonts for consistent typography
- Create reusable components for common elements
- Use the visual editor for template customization
- Test PDF generation after configuration changes
For Developers
- Use the service classes for PDF generation
- Inject dependencies via constructor
- Cache generated PDFs for frequently accessed documents
- Test with long content to verify page breaks
- Use absolute paths for images
Troubleshooting
| Issue | Solution |
|---|---|
| PDF blank/empty | Check Blade template syntax |
| Logo not showing | Use public_path() for image path |
| Custom font not rendering | Check font is active, clear font cache |
| Fonts not rendering | Use DejaVu Sans fallback, clear font cache |
| PDF generation slow | Reduce image sizes, simplify CSS |
| CSS not applying | Use inline styles, check CSS support |
| Memory error | Increase PHP memory_limit |
| Header/footer not showing | Ensure enabled: true in config |
Clearing Font Cache
rm -rf storage/fonts/*
rm -rf storage/app/fonts/*
php artisan cache:clear
Testing PDF Generation
// In Tinker - Invoice PDF
$invoice = Invoice::first();
$pdf = app(InvoicePdfService::class)->generate($invoice);
$pdf->save(storage_path('test-invoice.pdf'));
// In Tinker - Document PDF
$document = GeneratedDocument::first();
$path = app(PdfGeneratorService::class)->generate($document);
echo "Generated: {$path}";
Verifying Font Installation
// In Tinker
$fontManager = app(\App\Services\Pdf\FontManager::class);
$fonts = $fontManager->getAvailableFonts();
print_r($fonts);
Testing
Running PDF Tests
# All PDF-related tests
php artisan test --filter=Pdf
# Font management tests
php artisan test tests/Feature/Admin/PdfFontManagementTest.php
# Component management tests
php artisan test tests/Feature/Admin/PdfComponentManagementTest.php
# Service unit tests
php artisan test tests/Unit/Services/Pdf/FontManagerTest.php
php artisan test tests/Unit/Services/Pdf/LayoutManagerTest.php
See Also
- Invoicing - Invoice management
- Document Templates - Custom documents
- Reports - Report generation
- Digital Signatures - E-signatures on PDFs