renatio/dynamicpdf-plugin
| Install | |
|---|---|
composer require renatio/dynamicpdf-plugin |
|
| Latest Version: | v8.0.3 |
| PHP: | ^8.2 |
| License: | MIT |
| Last Updated: | Sep 16, 2026 |
| Links: | GitHub · Packagist |
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, synchronised to the database and customisable, with reset to the file version.
- Twig markup with theme partials, translations, global variables and
beforeRender/afterRenderevents. - Render per document with another layout, in another language, with page numbers, password protection and paper size and orientation.
- Output as a browser stream, download, file on a storage disk or a
System\Models\Fileready to attach to a model. PDF::fake()with render assertions for project tests,dynamicpdf:syncanddynamicpdf:checkconsole commands.
Requirements
This plugin requires PHP 8.2 or higher and October CMS 4.0 or higher.
Templates are rendered with Twig without a sandbox, so the Manage templates and Manage layouts permissions
should only be granted to trusted users.
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.
Documentation
Installation
There are couple ways to install this plugin.
- Use
php artisan plugin:install Renatio.DynamicPDFcommand. - Use
composer require renatio/dynamicpdf-pluginin project root. When you use this option you must runphp artisan october:migrateafter installation.
PDF content
PDF can be created in October using either PDF views or PDF templates. A PDF view is supplied by plugin in the file system in the /views directory. Whereas a PDF template is managed using the back-end interface via Settings > PDF > PDF Templates. All PDFs templates support using Twig for markup.
PDF views must be registered in the Plugin registration file with the registerPDFTemplates and registerPDFLayouts
method. This will automatically generate a PDF template and layout and allows them to be customized using the back-end
interface.
PDF layouts views
PDF layouts views reside in the file system and the code used represents the path to the view file. For example PDF layout with the code author.plugin::pdf.layouts.default would use the content in following file:
plugins/ <=== Plugins directory
author/ <=== "author" segment
plugin/ <=== "plugin" segment
views/ <=== View directory
pdf/ <=== "pdf" segment
layouts/ <=== "layouts" segment
default.htm <=== "default" segment
The content inside a PDF view file can include up to 3 sections: configuration, CSS/LESS, and HTML markup.
Sections are separated with the == sequence. For example:
name = "Default PDF layout"
==
body {
font-size: 16px;
}
==
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Document</title>
<style type="text/css" media="screen">
{{ css|raw }}
</style>
</head>
<body>
{{ content_html|raw }}
</body>
</html>
Note: Basic Twig tags and expressions are supported in PDF views.
The CSS/LESS section is optional and a view can contain only the configuration and HTML markup sections.
name = "Default PDF layout"
==
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>Document</title>
<style type="text/css" media="screen">
{{ css|raw }}
</style>
</head>
<body>
{{ content_html|raw }}
</body>
</html>
Configuration section
The configuration section sets the PDF view parameters. The following configuration parameters are supported:
| Parameter | Description |
|---|---|
| name | the layout name, required. |
Using PDF layouts
PDF layouts reside in the database and can be created by selecting Settings > PDF > PDF Templates and clicking the * Layouts* tab. These behave just like CMS layouts, they contain the scaffold for the PDF. PDF views and templates support the use of PDF layouts. The code specified in the layout is a unique identifier and cannot be changed once created.
PDF templates views
PDF templates reside in the file system and the code used represents the path to the view file. For example PDF template with the code author.plugin::pdf.invoice would use the content in following file:
plugins/ <=== Plugins directory
author/ <=== "author" segment
plugin/ <=== "plugin" segment
views/ <=== View directory
pdf/ <=== "pdf" segment
invoice.htm <=== "invoice" segment
The content inside a PDF view file can include up to 2 sections: configuration and HTML markup. Sections are
separated with the == sequence. For example:
title = "Invoice"
layout = "renatio.demo::pdf.layouts.default"
description = "Invoice template"
size = "a4"
orientation = "portrait"
==
<h1>Invoice</h1>
Note: Basic Twig tags and expressions are supported in PDF views.
Configuration section
The configuration section sets the PDF view parameters. The following configuration parameters are supported:
| Parameter | Description |
|---|---|
| title | the template title, required. |
| layout | the layout code, optional. |
| description | the template description, optional. |
| size | the template paper size, optional, default a4. |
| orientation | the template paper orientation, optional, default portrait. |
Note: size and orientation are read case-insensitively;
A4is stored asa4.
Using PDF templates
PDF templates reside in the database and can be created in the back-end area via Settings > PDF > PDF Templates. The code specified in the template is a unique identifier and cannot be changed once created.
Note: If the PDF template does not exist in the system, this code will attempt to find a PDF view with the same code.
Registering PDF templates and layouts
PDF views can be registered as templates that are automatically generated in the back-end ready for customization. PDF
templates can be customized via the Settings > PDF Templates menu. The templates can be registered by adding
the registerPDFTemplates method of the Plugin registration class (Plugin.php).
public function registerPDFTemplates()
{
return [
'renatio.demo::pdf.invoice',
'renatio.demo::pdf.resume',
];
}
The method should return an array of pdf view names.
Registered views are synchronised to the database when the PDF Templates or PDF Layouts backend page is displayed
and when php artisan dynamicpdf:sync or php artisan dynamicpdf:demo runs, not on every request. Synchronisation
creates the missing templates and layouts, deletes the templates that are no longer registered and were never
customised, and writes changed view files back to the rows of templates that are not customised, so list search and
sort work on the current values. Customised templates and every layout are left alone. A code whose view file is
missing is skipped and written to the application log once per process; a stored template whose view file went
missing, parses to no content or whose layout code resolves to nothing keeps its stored content. Until a registered
view is synchronised, PDF::loadTemplate() renders it straight from the file.
Like templates, PDF layouts can be registered by adding the registerPDFLayouts method of the Plugin registration
class (Plugin.php).
public function registerPDFLayouts()
{
return [
'renatio.demo::pdf.layouts.invoice',
'renatio.demo::pdf.layouts.resume',
];
}
The method should return an array of pdf view names.
Twig environment
Templates and layouts are rendered with the CMS Twig environment when the Cms module is installed and a theme is
active, so theme partials and content blocks are available. Otherwise, for example in a backend-only installation
that loads only the System and Backend modules, the system Twig environment is used. Filters and functions registered
by plugins through registerMarkupTags work in both. Filters and tags provided by the Cms module itself (|theme,
|page, {% partial %}) are unavailable without it.
Global variables
Variables every template and layout should receive, such as company details or a logo URL, are registered in the plugin registration class. A closure value is resolved when the document is rendered:
public function registerPDFVariables()
{
return [
'company' => 'Acme Ltd',
'vat_rate' => fn () => Settings::get('vat_rate'),
];
}
Data passed to loadTemplate(), loadLayout() or parseTemplate() takes precedence over a registered variable. The
names content_html, css, background_img and locale are reserved for the wrapper and ignored when registered.
Every closure is resolved on every render, whether or not the template uses it, so keep them cheap.
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 rendered HTML |
Event::listen('renatio.dynamicpdf.beforeRender', function ($pdf, $model, array $data) {
return ['watermark' => $data['order']->isDraft() ? 'DRAFT' : null];
});
Both events fire once per document: for a template together with its layout (loadTemplate(), parseTemplate()),
or for a layout rendered on its own (loadLayout(), parseLayout()). They also fire for the backend HTML and PDF
preview, so keep side effects such as counters or audit entries out of the listeners or check $pdf for the preview
context yourself. Every listener runs; the arrays they return are merged in turn, and the reserved names
content_html, css, background_img and locale are stripped from what a listener returns.
Usage
PDF templates and layouts can be accessed in the back-end area via Settings > PDF > PDF Templates.
The list marks templates edited in the back-end as Customized (they no longer follow their view file) and layouts
created from a registered view file as From view (they offer Reset to default instead of Delete), and links to
the HTML and PDF preview of every record. A template's Sample data (a JSON object on
the Options tab, nested objects and lists included) is passed to both previews, so {{ variables }} render with
realistic values. A template becomes Customized only when a value the view file provides is changed; editing the
sample data alone keeps it view-driven.
The Actions column of both lists opens the PDF preview and duplicates, resets or deletes the record; a record
that follows a view file offers Reset to default instead of Delete. Duplicate creates an editable copy with a _copy code (_copy2 and so on when that
code is taken); the copy of a template is customised and the copy of a layout is not locked.
Layouts define the PDF scaffold, that is everything that repeats on a PDF, such as a header and footer. Each layout has unique code, optional background image, HTML content and CSS/LESS content. Not all CSS properties are supported, so check CSSCompatibility.
Templates define the actual PDF content parsed from HTML.
Permissions
Access is granted under Settings → Administrators, on the PDF tab. Super users bypass both.
| Permission | What it unlocks |
|---|---|
| Manage templates | The PDF Templates page, and the template form, previews and duplicates. |
| Manage layouts | The layout form, its previews and duplicates. |
Without Manage layouts the Layouts tab is hidden on the PDF Templates page.
Configuration
The default configuration settings are set in config/dompdf.php. Copy this file to your own config directory to modify
the values. You can publish the config using this command:
php artisan vendor:publish --provider="Barryvdh\DomPDF\ServiceProvider"
You can still alter the dompdf options in your code before generating the PDF using dynamic methods for all options like so:
PDF::loadTemplate('renatio::invoice')
->setDpi(300)
->setDefaultFont('sans-serif')
->stream();
or you can use setOption method before generating the pdf using this command:
PDF::loadTemplate('renatio::invoice')
->setOption(['dpi' => 300, 'defaultFont' => 'sans-serif'])
->stream();
The options most often changed are dpi, default_font, default_paper_size, enable_remote (off by default;
required for images, stylesheets and fonts loaded by URL), allowed_remote_hosts, chroot and font_dir. The full
list with the current defaults is in the published config/dompdf.php and in
Dompdf\Options; every option has a matching
set*() method on the wrapper named after the camel-cased key, except the enable_* options, which are
setIsRemoteEnabled(), setIsPhpEnabled(), setIsJavascriptEnabled(), setIsHtml5ParserEnabled(),
setIsFontSubsettingEnabled() and setIsPdfAEnabled(). A setter that exists on neither dompdf nor its options
throws UnexpectedValueException.
Self-signed certificates
Remote resources (requires enable_remote in the dompdf configuration) are fetched with full TLS verification. On a
development host with a self-signed certificate set DYNAMICPDF_ALLOW_SELF_SIGNED=true in .env (or
allow_self_signed_certificates in config/renatio/dynamicpdf.php), or call allowSelfSignedCertificates() on the
wrapper for a single document. The setting applies to every wrapper instance, including loadHTML() and after
setOptions().
Methods
| 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 |
| 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 |
| allowSelfSignedCertificates() | Accept self-signed TLS certificates for remote resources |
| allowRemoteApplicationAssets() | Limit remote resources to the configured and application hosts and local files to the 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 |
| 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 |
All methods are available through Facade class Renatio\DynamicPDF\Classes\PDF.
Tips
Background image
To display background image added in layout use following code:
<body style="background: url({{ background_img }}) top left no-repeat;">
Background image should be at least 96 DPI size (793 x 1121 px).
If you want to use better quality image like 300 DPI (2480 x 3508 px) than you need to change template options like so:
return PDF::loadTemplate($model->code)
->setDpi(300)
->stream();
UTF-8 support
In your layout, set the UTF-8 meta tag in head section:
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
If you have problems with foreign characters than try to use DejaVu Sans font family.
Page breaks
You can use the CSS page-break-before/page-break-after properties to create a new page.
<style>
.page-break {
page-break-after: always;
}
</style>
<h1>Page 1</h1>
<div class="page-break"></div>
<h1>Page 2</h1>
Open basedir restriction error
On some hosting providers there were reports about open_basedir restriction problems with log file. You can change
default log file destination like so:
return PDF::loadTemplate('renatio::invoice')
->setLogOutputFile(storage_path('temp/log.htm'))
->stream();
Embed image inside PDF template
You can use absolute path for image eg. https://app.dev/path_to_your_image.
For this to work you must set isRemoteEnabled option.
return PDF::loadTemplate('renatio::invoice', ['file' => $file])
->setIsRemoteEnabled(true)
->stream();
I assume that $file is instance of October\Rain\Database\Attach\File.
Then in the template you can use following example code:
{{ file.getPath }}
{{ file.getLocalPath }}
{{ file.getThumb(200, 200, {'crop' => true}) }}
For retrieving stylesheets or images via http following PHP setting must be enabled
allow_url_fopen.
The backend PDF preview fetches remote resources only from the hosts listed in
allowed_remote_hostsof the dompdf configuration plus the application host (app.urland the custom site URLs); when no host can be resolved at all it fetches nothing remote. It reads local files only from the directories October publishes (web root, modules, plugins, themes, app assets, public uploads, media and the resize cache) unlesschrootis set in the configuration, and writes the dompdf log file only whenapp.debugis on. A schemelessAPP_URLsuch asmyapp.testis understood.
When allow_url_fopen is disabled on server try to use relative path. You can use October getLocalPath function on
the file object to retrieve it.
Attach the PDF to a model
toFile() returns a System\Models\File built from the rendered document, so the PDF can be attached with October's
attachOne / attachMany relations instead of being written to disk by hand:
$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, otherwise the record points at the wrong
directory. The file is written to the uploads disk as soon as toFile() returns, so attach and save it, or call
$file->delete() when you abandon it.
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 last, 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
OctoberCMS ajax framework cannot handle this type of response.
Recommended approach is to save PDF file locally and return redirect to PDF file.
Page numbers
Page numbers are stamped on every page after rendering, without enabling inline PHP:
return PDF::loadTemplate('renatio::invoice')
->pageNumbers('Page {PAGE_NUM} of {PAGE_COUNT}', position: 'bottom-center', size: 9)
->stream();
{PAGE_NUM} and {PAGE_COUNT} are replaced on each page. Positions: top-left, top-center, top-right,
bottom-left, bottom-center, bottom-right; font (a family available in the document, for example one declared
with @font-face in the layout; the dompdf default font otherwise), margin (points) and color (RGB between 0
and 1) are optional. Requires the CPDF or PDFLib backend; the GD backend cannot draw page text.
Call pageNumbers() after loading the document; it applies to that document only. Inline PHP
(setIsPhpEnabled(true)) is no longer needed for page numbers and should stay off.
Security warning: only enable
setIsPhpEnabled(true)when the template content is fully trusted. Any<script type="text/php">block in the HTML is executed on the server, so enabling it for templates that can be edited by backend users allows remote code execution. The backend HTML and PDF preview never enables it.
Testing
PDF::fake() replaces the wrapper for the rest of the test: no template is looked up, no Twig, dompdf, database or
filesystem work happens, stream() and download() return an empty application/pdf response, output() returns an
empty string, save() writes nothing and toFile() returns a record that can be attached and saved. The fake records
every loadTemplate(), loadLayout(), loadView(), loadFile(), parseTemplate() and parseLayout() call 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');
assertNothingRendered() covers the negative case and rendered() returns the raw records. pageNumbers() keeps
validating its arguments under the fake.
Console commands
php artisan dynamicpdf:sync synchronises the registered PDF views with the database and lists what was created,
updated, deleted or failed; it exits with code 1 when a registered code has no view file. Run it after a deployment
so the templates exist before the first backend visit.
php artisan dynamicpdf:check reports the dompdf configuration that fails silently: font, cache and temporary
directories (existence and write access, without creating anything), chroot, inline PHP and remote resources, and
every registered code without a view file. It exits with code 1 on a failure, so it can guard a deployment. Run both
commands after october:migrate.
Examples
Demo examples
There is a console command that will enable demo templates and layouts.
php artisan dynamicpdf:demo
To disable demo run following command:
php artisan dynamicpdf:demo --disable
The first example shows invoice with custom font and image embed.
The second example shows usage of header & footer, page break and full background image.
Render PDF in browser
use Renatio\DynamicPDF\Classes\PDF; // import facade
public function pdf()
{
$templateCode = 'renatio::invoice'; // unique code of the template
$data = ['name' => 'John Doe']; // optional data used in template
return PDF::loadTemplate($templateCode, $data)->stream('download.pdf');
}
Where $templateCode is an unique code specified when creating the template, $data is optional array of twig fields
which will be replaced in template.
In HTML template you can use {{ name }} to output John Doe.
Download PDF
use Renatio\DynamicPDF\Classes\PDF;
public function pdf()
{
return PDF::loadTemplate('renatio::invoice')->download('download.pdf');
}
Fluent interface
You can chain the methods:
return PDF::loadTemplate('renatio::invoice')
->save('/path-to/my_stored_file.pdf')
->stream();
Render with another layout
A template can be rendered with a different layout than the one stored with it, for example one letterhead per company, without changing the template in the database:
return PDF::loadTemplate('renatio::invoice', $data, layout: 'renatio::layouts.company_b')->stream();
Only the layout markup, CSS and background image are swapped; paper size and orientation still come from the
template, so call setPaper() when the other layout needs them changed.
Render in another language
The document language usually should not depend on the language of the backend user who generated it. Pass the
locale to render in; language file translations (trans, __), Carbon dates and the locale template variable
follow it while the template is parsed, and the application locale is restored afterwards, also when parsing fails:
return PDF::loadTemplate('renatio::invoice', $data, locale: 'de')->download('rechnung.pdf');
loadTemplate() and loadLayout() always add the locale variable, holding the application locale when no argument
is given; a locale key in your own data takes precedence. With RainLab.Translate installed its messages (|_) and
translated model attributes follow the site locale, not this argument, and inline PHP executed by dompdf during
output() runs after the locale has been restored.
Change paper size and orientation
return PDF::loadTemplate('renatio::invoice')
->setPaper('a4', 'landscape')
->stream();
Available paper sizes.
PDF on CMS page
To display PDF on CMS page you can use PHP section of the page like so:
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: never; }
</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
Plugin provides "Open Sans" font, which can be imported in Layout CSS section.
@font-face {
font-family: 'Open Sans';
src: url({{ 'plugins/renatio/dynamicpdf/assets/fonts/OpenSans-Regular.ttf'|app }});
}
@font-face {
font-family: 'Open Sans';
font-weight: bold;
src: url({{ 'plugins/renatio/dynamicpdf/assets/fonts/OpenSans-Bold.ttf'|app }});
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
src: url({{ 'plugins/renatio/dynamicpdf/assets/fonts/OpenSans-Italic.ttf'|app }});
}
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: bold;
src: url({{ 'plugins/renatio/dynamicpdf/assets/fonts/OpenSans-BoldItalic.ttf'|app }});
}
body {
font-family: 'Open Sans', sans-serif;
font-size: 16px;
}