Skip to main content
Back to ScopeForged

ScopeForged Documentation

Technical documentation, guides, and feature references for the ScopeForged client portal.

Core Business Features/PDF Generation

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

  1. Accessing PDFs
  2. Features
  3. How to Use
  4. PDF Templates
  5. Async PDF Generation
  6. Bulk PDF Generation
  7. Custom Fonts
  8. PDF Components
  9. Visual Template Editor
  10. Layout Options
  11. Company Configuration
  12. Technical Architecture
  13. Related Features

Accessing PDFs

Access PointURLRole
View Invoice PDF/admin/invoices/{id}/pdfAdmin
Download Invoice PDF/admin/invoices/{id}/pdf/downloadAdmin
Portal View PDF/portal/invoices/{id}/pdfClient User
Portal Download PDF/portal/invoices/{id}/pdf/downloadClient User
Manage PDF Fonts/admin/pdf-fontsAdmin
Manage PDF Components/admin/pdf-componentsAdmin
Template Editor/admin/documents/templates/{id}/editorAdmin

Permissions

ActionAdminClient 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

  1. Navigate to any invoice detail page
  2. Click "View PDF" button
  3. PDF opens in browser tab
  4. Use browser controls to print or save

Downloading a PDF

  1. Navigate to any invoice detail page
  2. Click "Download PDF" button
  3. PDF downloads with filename: invoice-{number}.pdf
  4. Open with any PDF reader

PDF from Invoice List

  1. Navigate to Admin → Invoices
  2. Click the PDF icon in the actions column
  3. 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.

ActionDescription
Create TemplateCreate new PDF template
Edit TemplateModify content, header, footer
PreviewView PDF with sample data
Restore VersionRestore previous version
Toggle ActiveEnable/disable template

Creating a Template

  1. Navigate to Admin → PDF Templates → Create
  2. 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
  3. 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:

  1. Edit a template
  2. Click Save Changes
  3. Previous version is automatically saved
  4. View version history in sidebar
  5. 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

  1. User requests PDF generation
  2. PdfGeneration record created with status "pending"
  3. GeneratePdfJob dispatched to queue
  4. Job processes and generates PDF
  5. User notified when complete
  6. PDF available for download

Generation Status

StatusDescription
pendingJob queued, waiting to process
processingJob currently running
completedPDF ready for download
failedGeneration failed with error

Viewing Generations

Navigate to Admin → PDF Templates → My Generations to view your PDF generations.

Receiving Notifications

When a PDF is ready:

  1. Email notification sent with download link
  2. Database notification shown in app
  3. 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.

ActionDescription
Upload FontUpload TTF, OTF, or WOFF font files
Toggle ActiveEnable/disable fonts for use in templates
Preview FontView font rendering at different sizes
Delete FontRemove unused fonts

Uploading a Font

  1. Navigate to Admin → PDF Fonts → Add Font
  2. 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
  3. Upload the font file (TTF, OTF, or WOFF)
  4. 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:

TypeDescription
headerDocument headers with company branding
footerPage footers with page numbers, copyright
signatureSignature blocks with name/date placeholders
letterheadFull letterhead designs
tableStyled table templates
termsTerms and conditions blocks

Managing Components

Navigate to Admin → PDF Components to manage components.

ActionDescription
Create ComponentCreate new reusable component
Edit ComponentModify component content and styles
DuplicateCopy an existing component
Toggle GlobalMake component available to all users
PreviewView rendered component

Creating a Component

  1. Navigate to Admin → PDF Components → Create
  2. 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
  3. 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

  1. Navigate to Admin → Document Templates
  2. Click on a template to view details
  3. Click "Open Editor" button

Editor Features

SectionDescription
Content EditorMonaco-based code editor for template HTML
Layout SettingsPage size, orientation, margins
TypographyFont family, body/heading sizes
ColorsPrimary, secondary, accent, background colors
Header ConfigLeft/center/right zones, height, border
Footer ConfigLeft/center/right zones, page numbers, border
Live PreviewReal-time PDF preview (iframe)

Layout Options

OptionValues
Page SizeLetter, Legal, A4, A5, A3
OrientationPortrait, Landscape
MarginsTop, Right, Bottom, Left (mm)

Header/Footer Variables

VariableDescription
{{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

SizeDimensions
Letter8.5" x 11" (612 x 792 pt)
Legal8.5" x 14" (612 x 1008 pt)
A4210mm x 297mm (595.28 x 841.89 pt)
A5148mm x 210mm (419.53 x 595.28 pt)
A3297mm x 420mm (841.89 x 1190.55 pt)

Margin Units

Margins can be specified in various units:

UnitExample
mm (default)25 or 25mm
inches1in
centimeters2.5cm
points72pt

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

ViewDescription
admin/pdf-fonts/index.blade.phpFont listing with filters
admin/pdf-fonts/create.blade.phpFont upload form
admin/pdf-fonts/edit.blade.phpFont edit form
admin/pdf-fonts/show.blade.phpFont details and preview
admin/pdf-components/index.blade.phpComponent listing
admin/pdf-components/create.blade.phpComponent creation form
admin/pdf-components/edit.blade.phpComponent edit form
admin/pdf-components/show.blade.phpComponent preview
admin/documents/templates/editor.blade.phpVisual template editor
documents/pdf-layout-enhanced.blade.phpEnhanced 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 Sans for 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;
}

Dependencies

FeatureRelationship
InvoicingProvides invoice data
Client ManagementClient bill-to information
Document TemplatesTemplate configuration

Complementary Features

FeatureDescription
Document TemplatesCustom document generation
ReportsReport PDF exports
NotificationsEmail PDF attachments
Digital SignaturesSigned document PDFs

Best Practices

For Administrators

  1. Configure company details before generating invoices
  2. Upload brand fonts for consistent typography
  3. Create reusable components for common elements
  4. Use the visual editor for template customization
  5. Test PDF generation after configuration changes

For Developers

  1. Use the service classes for PDF generation
  2. Inject dependencies via constructor
  3. Cache generated PDFs for frequently accessed documents
  4. Test with long content to verify page breaks
  5. Use absolute paths for images

Troubleshooting

IssueSolution
PDF blank/emptyCheck Blade template syntax
Logo not showingUse public_path() for image path
Custom font not renderingCheck font is active, clear font cache
Fonts not renderingUse DejaVu Sans fallback, clear font cache
PDF generation slowReduce image sizes, simplify CSS
CSS not applyingUse inline styles, check CSS support
Memory errorIncrease PHP memory_limit
Header/footer not showingEnsure 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