32

Product support

Visit this product's website for support.

Categories

Dynamic PDF Plugin

Create and edit PDF templates for October CMS in the backend — HTML and Twig in, a document out.

Demo URL: https://october-demo.renatio.com/backend/backend/auth/signin
Login: dynamicpdf
Password: dynamicpdf

Templates and layouts live in the database or in view files shipped by your plugins, and are rendered to PDF with dompdf through barryvdh/laravel-dompdf.

Features

  • PDF templates and layouts edited in the backend, with HTML and PDF preview rendered from sample data.
  • Templates and layouts registered by plugins as view files, synchronized to the database, with reset to the file.
  • Twig markup with theme partials, translations, global variables and beforeRender / afterRender events.
  • Per-document layout, language, page numbers, password protection, paper size and orientation.
  • Templates translated per language with October multisite, plus localized view files such as pdf.de.invoice.
  • QR codes and barcodes with the pdf_qr() and pdf_barcode() Twig functions.
  • Output as a browser stream, download, file on a storage disk or a System\Models\File to attach to a model.
  • PDF::fake() with render assertions for tests, plus console commands to sync, check, render, export and import.

Requirements

PHP 8.2+ and October CMS 4.4+.

Templates render in October's Twig sandbox (see Twig environment). Still, grant the Create and Update permissions for templates and layouts to trusted users only.

Like this plugin?

If you like this plugin, give this plugin a Like or Make donation with PayPal.

My other plugins

Please check my other plugins.

Support

Please use GitHub Issues Page to report any issues with plugin.

Reviews should not be used for getting support or reporting bugs, if you need support please use the Plugin support link.

Icon made by Darius Dan from www.flaticon.com.

Templates and layouts in one place, with a Customized column for the ones you edited.

Installation via Command Line

php artisan plugin:install Renatio.DynamicPDF

Documentation

Installation

  • php artisan plugin:install Renatio.DynamicPDF, or
  • composer require renatio/dynamicpdf-plugin followed by php artisan october:migrate.

Upgrading

Read the upgrade guide before updating; versions not listed there need no action.

Quick start

Create a template under Settings > PDF > PDF Templates (or run php artisan dynamicpdf:demo), e.g. with the code acme.shop::pdf.invoice and the markup <h1>Invoice {{ number }}</h1>, then render it:

use Renatio\DynamicPDF\Classes\PDF;

return PDF::loadTemplate('acme.shop::pdf.invoice', ['number' => '2026/1'])->stream('invoice.pdf');

download() sends it as an attachment (use it in AJAX handlers), save() writes it to disk.

PDF content

A PDF template is either managed in the backend (Settings > PDF > PDF Templates) or supplied by a plugin as a view file in its views directory and registered with registerPDFTemplates / registerPDFLayouts. Both use Twig.

PDF layout views

The code is the path to the view file: author.plugin::pdf.layouts.default reads plugins/author/plugin/views/pdf/layouts/default.htm.

A layout view has up to 3 sections separated by ==: configuration, CSS/LESS (optional) and HTML markup:

name = "Default PDF layout"
==
body {
    font-size: 16px;
}
==
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <style type="text/css" media="screen">
            {{ css|raw }}
        </style>
    </head>
    <body>
        {{ content_html|raw }}
    </body>
</html>

Configuration section

Parameter Description
name the layout name, optional; the code is used without it.
pageNumbers page numbers position, e.g. bottom-center, see Page numbers.
pageNumbersText page numbers text, Page {PAGE_NUM} of {PAGE_COUNT} translated by default.
pageNumbersSize font size in points, 9 by default.
pageNumbersFont font family used by the document, the default font otherwise.
pageNumbersMargin distance from the page edge in points, 20 by default.
pageNumbersColor color as #rrggbb, black by default.

Using PDF layouts

Database layouts are managed on the Layouts tab of Settings > PDF > PDF Templates. They hold the scaffold of the PDF, like CMS layouts. The code is unique and cannot be changed once created.

PDF template views

author.plugin::pdf.invoice reads plugins/author/plugin/views/pdf/invoice.htm. A template view has up to 2 sections, configuration and HTML markup:

title = "Invoice"
layout = "renatio.demo::pdf.layouts.default"
description = "Invoice template"
size = "a4"
orientation = "portrait"
==
<h1>Invoice</h1>

Configuration section

Parameter Description
title optional; the code is used without it.
layout the layout code, optional.
description optional.
size paper size, optional; defaults to dompdf default_paper_size.
orientation paper orientation, optional; defaults to dompdf default_paper_orientation.

size and orientation are case-insensitive (A4 is stored as a4).

Using PDF templates

Database templates are managed in Settings > PDF > PDF Templates. The code is unique and cannot be changed once created.

If no database template has the code yet, a view registered under it with registerPDFTemplates is rendered instead. An unregistered view is not looked up and loadTemplate() throws ModelNotFoundException. Layouts follow the same rule with registerPDFLayouts.

Registering PDF templates and layouts

Return the view names from Plugin.php:

public function registerPDFTemplates()
{
    return [
        'renatio.demo::pdf.invoice',
        'renatio.demo::pdf.resume',
    ];
}

public function registerPDFLayouts()
{
    return [
        'renatio.demo::pdf.layouts.invoice',
        'renatio.demo::pdf.layouts.resume',
    ];
}

Registered views are synchronized to the database when a PDF Templates list or form page is displayed and when dynamicpdf:sync or dynamicpdf:demo runs — not on every request, AJAX request or preview. Synchronization:

  • creates missing templates and layouts;
  • updates non-customized records from changed view files; customized records are left alone;
  • keeps templates that are no longer registered (only dynamicpdf:sync --prune deletes them);
  • skips a code whose view file is missing or unreadable and logs it once per process; a stored record whose view file is missing or empty, or whose layout code resolves to nothing, keeps its stored content.

Until a registered view is synchronized, PDF::loadTemplate() renders it straight from the file.

Twig environment

Templates use the CMS Twig environment when the Cms module is installed and a theme is active (theme partials and content blocks available), otherwise the system Twig environment. Plugin filters and functions from registerMarkupTags work in both; Cms-module tags such as |theme, |page and {% partial %} need the Cms module.

Both run in October's Twig sandbox regardless of cms.safe_mode. Templates receive plain data:

  • models become arrays of their visible attributes, $appends accessors and already loaded relations; collections become arrays; dates stay dates; attachments keep path and url;
  • model methods and lazy-loaded relations are unavailable — eager-load with with(), add accessors to $appends, or pass arrays; models, queries and relations returned by other plugins' functions allow no method calls either;
  • config(), env(), response(), redirect(), abort(), ajaxHandler(), flash(), dump(), form_* and macro functions and the {% flash %} and {% cache %} tags are rejected; filters such as map take only arrow functions; object methods cannot be static or receive PHP function names.

A model method, lazy relation or accessor outside $appends renders as empty, and the log gets a warning naming the template, line and model.

Set DYNAMICPDF_SANDBOX=false (sandbox in config/renatio/dynamicpdf.php) only when everyone who can edit templates may run code on the server.

Plugin assets

dompdf treats every http(s):// address (including |app) as remote, and remote files are not loaded while enable_remote is off (the default). Use |pdfasset for plugin files: it takes a path relative to the project root and returns the absolute path on disk.

<link href="{{ 'plugins/acme/shop/assets/css/pdf.css'|pdfasset }}" rel="stylesheet">
<img src="{{ 'plugins/acme/shop/assets/img/logo.png'|pdfasset }}">

Only existing files inside plugins and the dompdf chroot resolve; any other path (.., absolute, URL) returns an empty string and logs a warning. In the backend HTML preview and outside a DynamicPDF render it returns the same URL as |app. Quote the value inside CSS url('...').

Global variables

Variables every template and layout receives are registered in Plugin.php. Closures are resolved on every render, so keep them cheap:

public function registerPDFVariables()
{
    return [
        'company' => 'Acme Ltd',
        'vat_rate' => fn () => Settings::get('vat_rate'),
    ];
}

Data passed to loadTemplate(), loadLayout() or parseTemplate() wins over a registered variable. The names content_html, css, background_img and locale are reserved and ignored when registered.

The template HTML editor autocompletes sample data keys, global variables, locale, nested keys, filters and tags inside {{ }} / {% %}; Ctrl+Space lists all of them.

Events

Event Payload Return value
renatio.dynamicpdf.beforeRender PDFWrapper $pdf, Template\|Layout $model, array $data an array merged on top of the render data
renatio.dynamicpdf.afterRender PDFWrapper $pdf, Template\|Layout $model, string $html a string replacing the HTML for the next listener and the PDF
Event::listen('renatio.dynamicpdf.beforeRender', function ($pdf, $model, array $data) {
    if ($model->code !== 'acme.shop::pdf.invoice' || ! isset($data['order'])) {
        return;
    }

    return ['watermark' => $data['order']->isDraft() ? 'DRAFT' : null];
});
  • Both fire once per document — a template with its layout (loadTemplate(), parseTemplate()) or a layout on its own (loadLayout(), parseLayout()) — including backend previews, so avoid side effects in listeners.
  • Listeners run for every document: check $model->code and the keys you read. Returning nothing changes nothing.
  • beforeRender results are merged in turn; reserved names (content_html, css, background_img, locale) are stripped.
  • afterRender listeners run in registration order, each receiving the previous one's HTML; returning false stops the rest.

Usage

Templates and layouts are managed in Settings > PDF > PDF Templates.

  • A record edited in the backend is marked Customized and no longer follows its view file. Editing only sample data or translations (another site selected) does not customize it. A customized layout ignores its localized view files.
  • Reset to Default restores a registered record from its view file; registered records offer it instead of Delete.
  • A layout used by a template cannot be deleted. The layout list shows the number of templates using it (Used by).
  • Duplicate creates a Customized copy with a _copy code (_copy2, ... when taken).
  • Sample data (a JSON object) is passed to the HTML and PDF previews, which show the current unsaved form values.
  • The History tab keeps the last 20 saved versions with date and author, a line diff per version, and Restore (Update permission), which adds a new version and marks the record Customized. Versions hold default-language values only. Background image changes are recorded, but Restore does not bring back a replaced image. Synchronization and console commands add no versions.

Layouts hold what repeats on every page (header, footer, background image, CSS/LESS); templates hold the content. Check dompdf's CSSCompatibility.

Permissions

Granted under Settings → Administrators, on the PDF tab. Super users bypass all of them; every action is also checked on the server.

Permission What it unlocks
Access templates The PDF Templates page with the template list.
Create templates The New template form and duplicating a template.
Update templates Opening and saving the template form, and resetting it to its view.
Delete templates Deleting a template.
Preview templates The HTML and PDF preview of a template.
Access layouts The Layouts tab (also needs Access templates).
Create layouts The New layout form and duplicating a layout.
Update layouts Opening and saving the layout form, and resetting it to its view.
Delete layouts Deleting a layout that no template uses.
Preview layouts The HTML and PDF preview of a layout.

Configuration

Publish config/dompdf.php to change the defaults:

php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"

Per document, use the dynamic setters or setOption():

PDF::loadTemplate('renatio::invoice')
    ->setDpi(300)
    ->setOption(['defaultFont' => 'sans-serif'])
    ->stream();

Common options: dpi, default_font, default_paper_size, enable_remote (off by default; needed for resources loaded by URL), allowed_remote_hosts, chroot and font_dir. See the published config and Dompdf\Options. Each option has a camel-cased set*() method, except enable_*: setIsRemoteEnabled(), setIsPhpEnabled(), setIsJavascriptEnabled(), setIsFontSubsettingEnabled() and setIsPdfAEnabled(). enable_html5_parser has no effect since dompdf 3. An unknown setter throws UnexpectedValueException.

Self-signed certificates

Remote resources are fetched with full TLS verification. On a development host set DYNAMICPDF_ALLOW_SELF_SIGNED=true (allow_self_signed_certificates in config/renatio/dynamicpdf.php) for every wrapper, including loadHTML() and after setOptions(), or call allowSelfSignedCertificates() for one document. Both are ignored in production.

Methods

All methods are available through the facade Renatio\DynamicPDF\Classes\PDF.

Method Description
loadTemplate($code, array $data = [], $encoding = null, $layout = null, $locale = null) Load backend template, optionally with another layout and locale
loadLayout($code, array $data = [], $encoding = null, $locale = null) Load backend layout, optionally in another locale
loadTemplateModel(Template $template, array $data = [], $encoding = null, $locale = null) Load a template model as it is, unsaved changes included
loadLayoutModel(Layout $layout, array $data = [], $encoding = null, $locale = null) Load a layout model as it is, unsaved changes included
pageNumbers($text = 'Page {PAGE_NUM} of {PAGE_COUNT}', $position = 'bottom-center', $size = 9, $font = null, $margin = 20, $color = [0, 0, 0]) Stamp page numbers on every page of the loaded document
forBrowser($forBrowser = true) Render HTML for a browser, with URLs from \|pdfasset and background_img instead of local paths
allowSelfSignedCertificates() Accept self-signed TLS certificates for remote resources
allowRemoteApplicationAssets() Enable remote resources only from the configured and application hosts (none when there are none), and narrow a default chroot to the public asset directories
loadHTML($string, $encoding = null) Load HTML string
loadFile($file) Load HTML string from a file
loadView($view, array $data = [], array $mergeData = [], $encoding = null) Load a Laravel view
parseTemplate(Template $template, array $data = []) Parse backend template using Twig
parseLayout(Layout $layout, array $data = []) Parse backend layout using Twig
setOption($attribute, $value = null) Change one dompdf option, or an array of them
setOptions(array $options, $mergeWithDefaults = false) Replace the whole dompdf options object
getDomPDF() Get the DomPDF instance
setPaper($paper, $orientation = 'portrait') Set the paper size and orientation (default A4/portrait)
setWarnings($warnings) Show or hide warnings
output() Output the PDF as a string
toFile($filename = 'document.pdf', $public = true) Return the PDF as a System\Models\File to attach to a model
attachment($filename = 'document.pdf') Return the PDF as an e-mail attachment
encrypt($password, $ownerPassword = '', $permissions = []) Password-protect the PDF (CPDF backend)
fake() Static: replace the wrapper with a recorder for tests
addInfo(array $info) Set PDF metadata such as Title or Author
save($filename, $disk = null) Save the PDF to a file, optionally on a storage disk
download($filename = 'document.pdf') Make the PDF downloadable by the user
stream($filename = 'document.pdf') Return a response with the PDF to show in the browser

Methods return the wrapper, so they can be chained:

return PDF::loadTemplate('renatio::invoice')
    ->save('/path-to/my_stored_file.pdf')
    ->stream();

Tips

Background image

The layout background shows only where the layout HTML uses {{ background_img }} (the default HTML of a new backend layout does it on <body>). For a full-page background:

<style>@page { margin: 0; }</style>
<body style="background: url('{{ background_img }}') top left no-repeat; background-size: 100% 100%;">

Without @page { margin: 0; } it covers only the area inside dompdf's default 1.2 cm margins.

  • In a PDF {{ background_img }} is a local path, so enable_remote is not needed. A file on a remote disk (S3) is copied to storage/temp/dynamicpdf and deleted with the PDF object. In the backend HTML preview it is the URL.
  • A file outside the dompdf chroot keeps its URL and loads only with enable_remote and its host in allowed_remote_hosts.
  • dompdf redraws the background at page size × DPI (794 × 1123 px for A4 at 96 DPI). For a sharper background use a larger image and raise the DPI (e.g. 300 DPI, 2480 × 3508 px) with setDpi() or dpi in the config, which the backend preview also uses. This makes the PDF larger.

UTF-8 support

Set <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> in the layout head. For foreign characters use the DejaVu Sans font.

Page breaks

<style>
.page-break {
    page-break-after: always;
}
</style>
<h1>Page 1</h1>
<div class="page-break"></div>
<h1>Page 2</h1>

Render log

dompdf writes no log by default. To get frame count, memory, time and debug output, set log_output_file in the config or per document; the file is overwritten on every render:

return PDF::loadTemplate('renatio::invoice')
    ->setLogOutputFile(storage_path('temp/log.htm'))
    ->stream();

The backend PDF preview writes storage/temp/log.htm only when app.debug is on.

Embed image inside PDF template

Prefer local files, which need no enable_remote: an attachment's local path, |pdfasset, or a data URI. $file is an October\Rain\Database\Attach\File:

return PDF::loadTemplate('renatio::invoice', ['file' => $file])->stream();
<img src="{{ file.getLocalPath }}">
<img src="{{ 'plugins/acme/shop/assets/img/logo.png'|pdfasset }}">
<img src="data:image/png;base64,{{ logo_base64 }}">

getLocalPath copies a remote-disk file to storage/temp/uploads, which October does not delete. Local paths must lie inside the dompdf chroot (the project root by default).

A URL (file.getPath, file.getThumb(200, 200, {'mode': 'crop'})) is remote. Always enable remote files together with allowed hosts: with allowed_remote_hosts null or empty, dompdf fetches any address.

return PDF::loadTemplate('renatio::invoice', ['file' => $file])
    ->setIsRemoteEnabled(true)
    ->setAllowedRemoteHosts(['cdn.example.com'])
    ->stream();

dompdf uses cURL when available and needs allow_url_fopen only without it.

The backend PDF preview fetches remote resources only from allowed_remote_hosts plus the application hosts (app.url and custom site URLs; schemeless APP_URL is understood), and nothing remote when none resolve. Unless chroot is configured, it reads local files only from public directories (web root, modules, plugins, themes, app assets, public uploads, media, resize cache), so a protected upload renders from code but not in the preview.

QR codes and barcodes

pdf_qr(text, size = 150, margin = 0, options = {}) and pdf_barcode(text, type = 'C128', widthFactor = 2, height = 50) return a PNG data URI (an SVG data URI without the GD extension), so enable_remote is not needed. They require:

composer require endroid/qr-code picqer/php-barcode-generator
<img src="{{ pdf_qr('https://example.com/invoices/' ~ invoice.number ~ '/pay') }}" width="120">
<img src="{{ pdf_barcode(invoice.ean, 'EAN13') }}">

size and margin are pixels of the image; the printed size comes from the <img> width, so generate it larger than displayed (the demo invoice makes 192 px and shows 96 px).

pdf_qr() also takes a hash of options, in place of the size or after the margin:

Option Value Default
size image width and height in pixels 150
margin white space around the code in pixels 0
color colour of the code, #rgb or #rrggbb #000000
background #rgb, #rrggbb or transparent #ffffff
error_correction low, medium, quartile or high low
<img src="{{ pdf_qr(invoice.url, {size: 300, color: '#1f2937', error_correction: 'medium'}) }}" width="100">

Barcode types: EAN13, EAN8, UPCA, C128, C39 (constants of Renatio\DynamicPDF\Classes\Codes). Arguments are positional. A missing package, unsupported type, non-positive or non-integer size, or text the code cannot hold (wrong EAN-13 check digit, non-ASCII, too long for QR) stops the render with an explanatory message.

pdf_qr_available() and pdf_barcode_available() tell whether a supported version of the package is installed:

{% if pdf_qr_available() %}
    <img src="{{ pdf_qr('https://example.com/invoices/123') }}" width="80">
{% endif %}

Attach the PDF to a model

toFile() returns a System\Models\File for attachOne / attachMany relations:

$order->invoice = PDF::loadTemplate('renatio::invoice', ['order' => $order])->toFile('invoice.pdf', public: false);
$order->save();

Pass public: false for a relation declared with 'public' => false. The file is written to the uploads disk immediately, so attach and save it, or call $file->delete().

Attach the PDF to an e-mail

attachment() returns an application/pdf Illuminate\Mail\Attachment, rendered when added to the message:

Mail::send('acme.shop::mail.invoice', $data, function ($message) use ($order) {
    $message->to($order->email);
    $message->attach(PDF::loadTemplate('acme.shop::pdf.invoice', ['order' => $order])->attachment('invoice.pdf'));
});

In a mailable, return it from attachments() (not the constructor — the attachment holds a closure and cannot be serialized for the queue); a queued mailable then renders the PDF in the worker:

public function attachments(): array
{
    return [PDF::loadTemplate('acme.shop::pdf.invoice', ['order' => $this->order])->attachment('invoice.pdf')];
}

Save to a storage disk and set metadata

PDF::loadTemplate('renatio::invoice')
    ->addInfo(['Title' => 'Invoice 2026/1', 'Author' => 'Acme'])
    ->save('invoices/2026-1.pdf', 's3');

Password protection

return PDF::loadTemplate('renatio::invoice')->encrypt('reader-password', 'owner-password', ['print'])->stream();

encrypt() renders the document, so call it right before the output method. Permissions are opt-in: without ['print', 'copy', ...] the reader cannot print or copy. Requires the CPDF backend.

Download PDF via Ajax response

The AJAX framework saves a Content-Disposition: attachment response as a file, so a handler can return download():

public function onDownload()
{
    return PDF::loadTemplate('renatio::invoice')->download('invoice.pdf');
}

Download a record's PDF in the backend

In your plugin's controller:

use October\Rain\Exception\ForbiddenException;
use Renatio\DynamicPDF\Classes\PDF;

public function update_onDownloadInvoice($recordId)
{
    if (! $this->user->hasAccess('acme.shop.orders.invoice')) {
        throw new ForbiddenException;
    }

    $order = $this->formFindModelObject($recordId);

    return PDF::loadTemplate('acme.shop::pdf.invoice', ['order' => $order], locale: $order->locale ?: null)
        ->download('invoice-' . str_slug(str_replace('/', '-', $order->number)) . '.pdf');
}

And in the update form toolbar (controllers/orders/update.php):

<?= Ui::ajaxButton(label: 'Download invoice', handler: 'onDownloadInvoice', class: 'btn-default') ?>

The PDF shows the saved record, not unsaved form changes. Register the permission in Plugin::registerPermissions().

Other rendering engines

dompdf supports CSS 2.1 and parts of CSS 3, without flexbox or grid. For those, pass the HTML from PDF::parseTemplate() to another tool such as Gotenberg or Browsershot:

use Renatio\DynamicPDF\Models\Template;

$html = PDF::parseTemplate(Template::byCode('acme.shop::pdf.invoice'), ['order' => $order]);

The HTML uses the current application locale and public URLs for assets. Paper size, orientation and layout page numbers are not in the HTML; set them in the other tool.

Page numbers

Set a position under Options > Page numbers in the layout form (or pageNumbers = "bottom-center" in a layout view) and every template using the layout gets page numbers. An empty text falls back to "Page {PAGE_NUM} of {PAGE_COUNT}" in the render language.

From code (applies to that document only and overrides the layout setting):

return PDF::loadTemplate('renatio::invoice')
    ->pageNumbers('Page {PAGE_NUM} of {PAGE_COUNT}', position: 'bottom-center', size: 9)
    ->stream();

Positions: top-left, top-center, top-right, bottom-left, bottom-center, bottom-right. Optional font (a family available in the document, e.g. from @font-face), margin (points) and color (RGB 0–1). Requires the CPDF or PDFLib backend.

Security warning: page numbers do not need inline PHP. Enable setIsPhpEnabled(true) or enable_php only for fully trusted content: any <script type="text/php"> block runs on the server. The backend previews never run inline PHP.

Testing

PDF::fake() replaces the wrapper for the rest of the test: no lookup, Twig, dompdf, database or filesystem work. stream() / download() return an empty application/pdf response, output() an empty string, save() writes nothing, toFile() returns an attachable record and attachment() an empty attachment. Every loadTemplate(), loadLayout(), loadView(), loadFile(), parseTemplate() and parseLayout() call is recorded with its data, layout and locale:

$fake = PDF::fake();

$this->get('/backend/acme/orders/pdf/1');

$fake->assertRendered('acme::pdf.invoice', fn (array $data) => $data['order']->id === 1);
$fake->assertRenderedTimes('acme::pdf.invoice', 1);
$fake->assertNotRendered('acme::pdf.reminder');

Also assertNothingRendered() and rendered() (the raw records). pageNumbers() still validates its arguments.

To test a failed render, use $fake->throwOnRender(new RuntimeException('...')): every later render is recorded and then throws. Do not combine PDF::fake() with PDF::shouldReceive(), which replaces the fake with a mock.

Console commands

Run dynamicpdf:sync and dynamicpdf:check after october:migrate on every deployment.

php artisan dynamicpdf:sync synchronizes registered views and lists what was created, updated or failed and which non-customized templates are no longer registered. --prune deletes those (with translations) after confirmation; --force skips it. Exits with code 1 when any code fails to sync.

php artisan dynamicpdf:check reports silent dompdf misconfiguration: font, cache and temp directories (existence and write access), chroot, inline PHP, remote resources, registered codes without a view file, and the pdf_qr() / pdf_barcode() packages. It also lists customized templates and layouts that match their view apart from whitespace, candidates for Reset to Default. Exits with code 1 on a failure.

Render

php artisan dynamicpdf:render <code> renders a template like your application does.

Option Effect
--output= output path, storage/temp/<code>.pdf by default
--data= variables as a JSON object or @path/to/file.json; overrides --sample keys
--sample use the template's Sample data
--layout= as in PDF::loadTemplate()
--locale= as in PDF::loadTemplate()
--html write the HTML (with browser asset URLs) instead of the PDF
-v show the full exception on failure

It prints the written path and dompdf warnings (none with --html), and exits with code 1 on an unknown code, invalid data, Twig or dompdf error, or unwritable output path.

php artisan dynamicpdf:render acme.shop::pdf.invoice --sample --output=invoice.pdf
php artisan dynamicpdf:render acme.shop::pdf.invoice --data=@order.json --locale=de --html

Export and import

php artisan dynamicpdf:export [codes...] writes templates and layouts to one JSON file: content, paper and page number settings, sample data, the Customized flag, translations and the layout background image (base64). A template brings its layout along. Default path storage/app/dynamicpdf-export-<date>.json; --path=- prints to stdout.

php artisan dynamicpdf:export
php artisan dynamicpdf:export acme.shop::pdf.invoice acme.shop::pdf.quotation --path=pdf-templates.json

php artisan dynamicpdf:import <file> imports layouts first and prints what was created, updated or skipped. Existing codes are skipped unless --force overwrites them. Records are validated like a backend save (code, Twig, LESS) in one transaction: the first invalid record aborts the whole import.

php artisan dynamicpdf:import pdf-templates.json
php artisan dynamicpdf:import pdf-templates.json --force
  • A non-customized record stays so only if the target registers the same view; otherwise it is imported as customized.
  • Background images must be JPG, PNG, GIF or WebP; a layout whose background file is missing is exported without it, with a warning.
  • Translations for the target's default language are not imported.

Examples

Demo examples

php artisan dynamicpdf:demo
php artisan dynamicpdf:demo --disable

The demo adds an invoice (custom font, embedded image) and a document with header and footer, page break and full-page background. --disable deletes them, except a customized demo template or a demo layout that is customized or still used, and lists what it kept.

Render PDF in browser

use Renatio\DynamicPDF\Classes\PDF;

public function pdf()
{
    return PDF::loadTemplate('renatio::invoice', ['name' => 'John Doe'])->stream('download.pdf');
}

In the template, {{ name }} outputs John Doe. Use ->download('download.pdf') to download instead.

Render with another layout

Swaps the layout markup, CSS and background image without changing the stored template; paper size and orientation still come from the template (use setPaper() to change them):

return PDF::loadTemplate('renatio::invoice', $data, layout: 'renatio::layouts.company_b')->stream();

Render in another language

Translations (trans, __), Carbon dates and the locale variable follow the locale argument while parsing; the application locale is restored afterwards, also on failure:

return PDF::loadTemplate('renatio::invoice', $data, locale: 'de')->download('rechnung.pdf');

loadTemplate() and loadLayout() always add locale (the application locale by default); a locale key in your data wins. Inline PHP run by dompdf during output() runs after the locale is restored.

Translate the templates themselves

Template title and markup, and layout markup and CSS, can be stored per language. Enable it in config/multisite.php and clear the application cache:

'features' => [
    'renatio_dynamicpdf_template' => true,
],

Editing with another site selected in the backend then saves a translation, previews follow the selected site, and the locale argument picks the translation:

PDF::loadTemplate('acme.shop::pdf.invoice', $data, locale: 'pl')->download('faktura.pdf');

A language without a translation falls back to the stored content. Reset to Default and synchronization never touch translations. Per-language background images are not supported.

With the feature on and a locale argument passed, a registered view can ship localized siblings: pdf.invoice renders from pdf.de.invoice, pdf.layouts.default from pdf.layouts.de.default. de-AT tries pdf.de-AT.invoice, then pdf.de.invoice. A customized record or a stored translation wins over the sibling, which never changes the stored row.

plugins/acme/shop/views/pdf/
    invoice.htm
    de/invoice.htm
    de-AT/invoice.htm
    layouts/default.htm
    layouts/de/default.htm

RainLab.Translate |_ messages follow the site locale, not the locale argument.

Change paper size and orientation

return PDF::loadTemplate('renatio::invoice')
    ->setPaper('a4', 'landscape')
    ->stream();

Available paper sizes (the $PAPER_SIZES array).

PDF on CMS page

use Renatio\DynamicPDF\Classes\PDF;

function onStart()
{
    return PDF::loadTemplate('renatio::invoice')->stream();
}

Header and footer on every page

<html>
<head>
  <style>
    @page { margin: 100px 25px; }
    header { position: fixed; top: -60px; left: 0px; right: 0px; background-color: lightblue; height: 50px; }
    footer { position: fixed; bottom: -60px; left: 0px; right: 0px; background-color: lightblue; height: 50px; }
    p { page-break-after: always; }
    p:last-child { page-break-after: auto; }
  </style>
</head>
<body>
  <header>header on each page</header>
  <footer>footer on each page</footer>
  <main>
    <p>page1</p>
    <p>page2</p>
  </main>
</body>
</html>

Using custom fonts

The plugin ships Open Sans. Declare fonts in a <style> element of the layout HTML — the CSS tab is LESS without Twig, so |pdfasset works only in the HTML. Repeat the rule for OpenSans-Bold.ttf (font-weight: bold), OpenSans-Italic.ttf (font-style: italic) and OpenSans-BoldItalic.ttf (both):

<style>
@font-face {
    font-family: 'Open Sans';
    src: url('{{ 'plugins/renatio/dynamicpdf/assets/fonts/OpenSans-Regular.ttf'|pdfasset }}');
}

body {
    font-family: 'Open Sans', sans-serif;
}
</style>

The backend HTML preview embeds ttf, otf, woff and woff2 fonts from @font-face rules in <style> elements when they are public application files (up to 5 MB each); fonts from <link>ed stylesheets show a fallback there.

dompdf embeds full font files unless enable_font_subsetting is on, which keeps only the used glyphs (a one-page document in four Open Sans styles shrinks from about 525 KB to 30 KB). Set it in the config or per document:

return PDF::loadTemplate('renatio::invoice')
    ->setIsFontSubsettingEnabled(true)
    ->stream();
  • Found the plugin useful on 23 Feb, 2023

    A million times, thank you for this!

    The amount of time you must have spent on this is incredible. Within 2 hours I went from knowing nothing to a bootstrap 5 styled dynamic invoice converted to PDF.

    My only question would be how to include local CSS/LESS files rather than having to use a CDN link.

    The time you have saved me Renatio is invaluable. Lets buy this man some coffee and energy drinks to help keep this up!=D

  • Found the plugin useful on 29 Sep, 2020

    Awesome plugin!

  • Found the plugin useful on 5 Mar, 2020

    Great plugin for working with PDF's.

    I was pleasantly surprised by how many features are provided for a free plugin, and the code is bug free.

    Author is still actively adding updates which is quite nice.

    Thanks for saving me having to create my own PDF implementation for OctoberCMS

  • Found the plugin useful on 10 May, 2019

    Hey Renatio, thanks for a good plugin. I've used it before without issue. However, the project I'm working on now does not use the CMS module, which is disabled and there is no theme installed. After installing this plugin with composer, I am getting a cms active theme not set error. I made an issue in the repo and would appreciate any assistance in getting it working without the CMS module loaded. Thanks!

  • Found the plugin useful on 11 May, 2018

    Well Done Mate! Good Job!!!

    I used DOMPDF Lib in codeigniter couple of years ago.it was really a headache.

    in this plugin i see all the problems have been sorted out which i faced previously.

    Thanks For Sharing!

  • Found the plugin useful on 7 May, 2018

    Awesome plugin! Just faced the issue with encoding. Couldn't get cyrillic letters to show properly just like in Kenny Heisher review comment. Problem is that dompdf vendor got too little font collection. But for those who want cyrillic symbols you can use font-family: DejaVu Sans; that solved my problem

  • Found the plugin useful on 22 Mar, 2018

    Useful App Converted to a Comment System

    I spent time building a system that can run as a comment form because I was tired of looking for one outside of the forum topic. Thanks!

    • 1.Create a page for the form
    • 2.Create a partial that is going to load the back-end data
    • 3.Set the where() method to specify what data is being queried
    • 4.Add the ajax handler to refresh partial after submission to the component
    • 5.Add the php call to the onRefreshTime()

    1. Page Markup: Summarized

    <div id="magicForms">{% partial 'testing/magicform' %}</div> <form data-request="{{ genericForm2 }}::onFormSubmit">...</form>

    2. Partial Markup: Summarized

    <ul> {% for record in records %} <li><strong>{{ record.form_data_arr.name }}:</strong> {{ record.form_data_arr.comment }}</li> {% endfor %} </ul>

    3. Parital Code: This is where the the collection methods get handy. Using the where() method I can query for the specific group. Here I am searching for the forms with 'group' and 'testing-comments'; the reverse is to put the newest comment on top.

    use \Martin\Forms\Models\Record as FormsRecords; function onStart() { $comments = FormsRecords::all(); $this['records'] = $comments->where('group', 'testing-comments')->reverse(); }

    4. Generic AJAX Form Component: Add this in More Settings -> JS on Success

    jQuery(document).ready(function($) { setInterval(function () { $.request('onRefreshTime', { update: { 'testing/magicform' : '#magicForms' } }) }, 300); });

    5. Page Code: This is needed for the javascript to call the onRefreshTime.

    function onRefreshTime() { return [ '#magicForms' => $this->renderPartial('testing/magicform') ]; }

  • Found the plugin useful on 27 Jul, 2017

    well... the worst plugin EVER!!! (joke) THANK YOU MAN :D

  • Found the plugin not useful on 14 Jul, 2017

    Not working with rainlab translate

  • author

    Replied on 17 Jul, 2017

    Hi,

    I'm so sorry it doesn't support external plugin like RainLab.Translate, but tell me why it should?

    Don't you think this is a new feature than can be added on request?

    You added request on GitHub and 10 minutes later you gave this plugin a negative review, because it doesn't meet all your requirements. I don't see that I wrote in documentation that it supports Translate plugin. It's open source project and you're welcome to make pull request for new features. If you don't have skill to make it, than just leave it to me.

    There is one simple rule here: if you don't like this plugin or it doesn't suit your task, then don't use it. It's free plugin and I spent my free time to make it and support.

    This plugin has many positive reviews from people that thinks it was usefull for them, so your review doesn't make a difference here. Imagine situation when fresh October developer make his first plugin and share it with you. Plugin works fine, have no bugs and make a job done. Then you use it and it looks like it doesn't support RainLab.Translate plugin and you write first negative review. This can make a huge impact on newcomer and discourage him from creating new plugins at all. Please think what you're doing and appreciate someone else work.

    Cheers!

  • Found the plugin useful on 14 Jun, 2017

    Work like a charm. Thanks for the great job

  • Found the plugin useful on 5 May, 2017

    Thank you for the Plugin!

  • Found the plugin useful on 14 Apr, 2017

    Does the job! Thumbs up

  • Found the plugin useful on 28 Feb, 2017

    Thanks for Great plugin. But I am stucked with using UTF-8 font.

    Some people said I should run "php load_font.php FontName /path/to/font.ttf" (http://stackoverflow.com/questions/8968627/how-to-make-dompdf-support-unicode-font-step-by-step) But I cannot find the file "load_font.php" in this plugin. Please help me.

  • Found the plugin useful on 25 Sep, 2016

    Very useful!! Easy to use and did precisely what I needed it for!

  • Found the plugin useful on 12 Jul, 2016

    Nice plugin :-)

    I'm using window 7 and I had full permission but when I use preview button on templates backend it so message.

    unlink(C:\Users\lvthu\AppData\Local\Temp/04b854edea2a8e08d8007d212360ace1): Permission denied

    please help me.

  • author

    Replied on 12 Jul, 2016

    Thanks for the review :)

    This is not the place for support topic. Please use support area for this: https://github.com/mplodowski/DynamicPDF/issues

  • Found the plugin useful on 13 Jun, 2016

    Great plugin and outstanding support! Thank you very much!

  • Found the plugin useful on 8 May, 2016

    Nice plugin :-)

    If the demo template throws this error : DOMNode::cloneNode(): ID header already defined, it can be fixed by changing all id's in the template and layout into class, and changing the css accordingly.

  • author

    Replied on 9 May, 2016

    Thanks for the review :) This is now fixed in version 1.1.5.

  • Found the plugin useful on 11 Dec, 2015

    Thanks!

  • Found the plugin useful on 18 Sep, 2015

    Thanks! I find this really useful

  • Found the plugin useful on 20 Aug, 2015

    Perfect plugin. The best side is it supports Turkish characters :)

    Thank you!

  • Found the plugin useful on 3 Jul, 2015

    You plugin is amazing.. From html to pdf... It gives you the power for reporting generation directly from OctoberCMS... It's a must-have plugin!!!

  • Found the plugin useful on 12 May, 2015

    I Love you ! Just perfect and great idea !

9.0.1

Keep TLS verification on in production, log sandbox misses, whitespace-tolerant layout detection.

Oct 16, 2026

9.0.0

Security release. Templates render in a Twig sandbox and previews never run inline PHP.

Oct 15, 2026

8.0.3

Refactor to use pluck() function.

Oct 07, 2025

8.0.2

Update dependencies.

Jul 12, 2025

8.0.1

Support October 4.

Jul 07, 2025

7.1.2

Allow self signed certificates on development environment.

Oct 27, 2023

7.1.1

Minor fix.

Oct 09, 2023

7.1.0

Fix creating public directory.

May 31, 2023

7.0.1

Fix page numbers demo.

Oct 13, 2022

7.0.0

Update to Laravel Dompdf v2.0.

Oct 13, 2022

6.0.1

Minor update.

Jun 16, 2022

6.0.0

Require October CMS 3.0.

Jun 07, 2022

5.0.5

Brazilian portuguese language. Thanks to @gcairesdev.

Dec 21, 2021

5.0.4

Fix preview pdf layout.

Nov 24, 2021

5.0.3

Update documentation.

Nov 24, 2021

5.0.2

Add demo console command.

Nov 24, 2021

5.0.1

Support October v2.0 and upgrade to DOMPDF 1.0.

Aug 13, 2021

4.0.8

Allow to set dompdf option with dynamic method.

Dec 07, 2020

4.0.7

Add Polish translation.

Nov 22, 2020

4.0.6

Allow to reset view template to default.

Nov 22, 2020

4.0.5

Fix custom template error.

Nov 20, 2020

4.0.4

Fix composer installation.

May 14, 2020

4.0.3

Fix custom templates error.

Feb 21, 2020

4.0.2

Allow to register mail templates and layouts.

Feb 17, 2020

4.0.1

Fix error when active theme is not set.

Feb 07, 2020

4.0.0

!!! Require PHP 7.1. Please see upgrade guide before update.

Feb 07, 2020

3.0.5

Support LESS in PDF layouts.

Feb 07, 2020

3.0.4

Add paper size and orientation configuration.

Feb 07, 2020

3.0.3

Allow to use CMS partials and filters.

May 06, 2018

3.0.2

Fix creating fonts directory

Dec 13, 2017

3.0.1

Add support for RainLab.Translate plugin

Sep 08, 2017

3.0.0

!!! Add support for Laravel 5.5.

Sep 07, 2017

2.1.6

Allow Laravel 5.5.

Jul 30, 2017

2.1.5

Fix open_basedir restriction on some hostings. Set isRemoteEnabled flag to allow absolute paths.

May 14, 2017

2.1.4

German language. Thanks to @TimFoerster.

May 14, 2017

2.1.3

Upgrade to DomPDF 0.8.

Mar 26, 2017

2.1.2

Database maintenance. Updated all timestamp columns to be nullable.

Mar 11, 2017

2.1.1

Upgrade to DomPDF 0.7.

Sep 10, 2016

2.1.0

!!! This is an important update that contains breaking changes.

Sep 10, 2016

2.0.1

Add preview HTML buttons.

Jun 12, 2016

2.0.0

!!! This is an important update that contains breaking changes.

May 28, 2016

1.1.5

UI improvements. Thanks to @kocholes.

May 09, 2016

1.1.4

Add Spanish and Spanish-Argentina locale. Thanks to @kocholes.

Apr 10, 2016

1.1.3

Add stream parameters and Czech locale. Thanks to @vojtasvoboda.

Apr 10, 2016

1.1.2

Minor changes.

Oct 05, 2015

1.1.1

Use Twig::parse() facade. Only update for October build 300 and above.

Oct 05, 2015

1.1.0

!!! Important update

Oct 05, 2015

1.0.2

Minor changes.

Sep 26, 2015

1.0.1

Initialize plugin.

May 10, 2015

Upgrade guide

Versions not listed here need no action. Back up the database before upgrading.

Upgrading To 1.1.0

Plugin requires October build 300+.

Upgrading To 2.0.0

PDFTemplate::render() method was removed. Please switch to Renatio\DynamicPDF\Classes\PDF facade.

Upgrading To 2.1.0

Method setOrientation was removed. Use setPaper instead.

Upgrading To 2.1.1

Plugin requires Stable version of October and PHP >=5.5.9.

Upgrading To 3.0.0

Plugin requires OctoberCMS build 420+ with Laravel 5.5 and PHP >=7.0.

Upgrading To 4.0.0

Plugin requires PHP >=7.1 to work with the latest version of dompdf library.

For October composer based installation you must manually update project composer.json file to use at least PHP 7.1:

"config": {
    "preferred-install": "dist",
    "platform": {
        "php": "7.1"
    }
},

After this change run composer update command.

Upgrading To 4.0.8

This is the latest version with support for October 1.x.

Using setOptions method to change dompdf options is no longer recommended. This will override all Laravel dompdf configuration and use only options specified by method argument and dompdf defaults for options not set by the developer.

Instead of using this method, please use dynamic method call for option you would like to change. Please read more in documentation.

Upgrading To 5.0.1

Plugin requires OctoberCMS v2.1.x with Laravel 6 and PHP >=7.2.

Upgrading To 6.0.0

Plugin requires October CMS version 3.0 or higher, Laravel 9.0 or higher and PHP >=8.0.

Drop support for October CMS version 2.x.

Upgrading To 7.0.0

Plugin requires Laravel Dompdf v2 and dompdf 2. setOptions() is deprecated and still replaces the whole options object, as noted for 4.0.8; the new setOption() changes a single option and the dynamic set*() methods keep working.

Upgrading To 8.0.1

Plugin adds support for October CMS 4.0 while keeping 3.x, and upgrades to Laravel Dompdf v3 and dompdf 3. Options removed by dompdf 3 (for example enable_css_float and setAdminUsername()) no longer apply, and the HTML5 parser is always on.

Upgrading To 9.0.0

Security release. Requires PHP 8.2 and October CMS 4.4; older sites stay on 8.0.3. Run php artisan october:migrate.

  • Templates render in a Twig sandbox and receive plain arrays instead of models: eager-load relations or pass arrays. config(), env(), {% cache %} and similar calls are rejected. DYNAMICPDF_SANDBOX=false turns the sandbox off. A model method or lazy relation renders as empty without an error, so compare your PDFs after upgrading; from 9.0.1 each one is logged as a warning.
  • Replace <script type="text/php"> page numbers with the layout's Page numbers option or ->pageNumbers().
  • The PDF preview loads remote files only from the application host and allowed_remote_hosts; use |pdfasset for local files.
  • Layout CSS can no longer read server files: remove LESS (inline) imports and data-uri().
  • The migration deletes duplicate template and layout codes, keeping the customized one, and marks layouts it cannot prove unedited as Customized; use Reset to Default on those you never changed. Code reading a layout's is_locked must read is_custom, which holds the opposite value. dynamicpdf:check lists the customized ones that match their view apart from whitespace.
  • A registered code without a database row renders from its view instead of throwing ModelNotFoundException, and byCode() returns an unsaved model. Run dynamicpdf:sync after october:migrate if your code writes to template rows, and pick another layout with loadTemplate($code, $data, null, $layoutCode) instead of saving layout_id.
  • Permissions are granular. Existing roles keep their access; narrow it under Settings → Administrators.
  • dynamicpdf:sync no longer deletes templates whose view is gone; use --prune.
  • An unknown set*() dompdf option now throws UnexpectedValueException.
  • Set DYNAMICPDF_ALLOW_SELF_SIGNED=true on a development host with a self-signed certificate.

Upgrading To 9.0.1

allowSelfSignedCertificates() and DYNAMICPDF_ALLOW_SELF_SIGNED are ignored in production again, as in 8.x; 9.0.0 turned off certificate verification there too.