certigniter / laravel-certificate-renderer
Open, parse, and render Certigniter .igniter certificate packages as PDFs from any Laravel app.
Package info
github.com/Muchwat/certigniter-renderer-laravel
pkg:composer/certigniter/laravel-certificate-renderer
Requires
- php: ^8.2
- ext-zip: *
- barryvdh/laravel-dompdf: ^2.0|^3.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^3.0
- orchestra/testbench: ^10.0|^11.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^11.5.50|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Render encrypted Certigniter .igniter certificate templates as PDFs in a
Laravel application, entirely in PHP: no browser, no headless Chrome, and no
external service.
The package can:
- open and inspect uploaded
.igniterfiles; - discover recipient fields before issuing;
- render individual or bulk certificates;
- replace foreign logos, signatures, and other images at issue time;
- assign application-generated QR code and barcode values (e.g. unique verification links or codes) at issue time, or bind either to a recipient column;
- render text, images, shapes, QR codes, barcodes, masks, mirrors, groups, and embedded fonts;
- report non-fatal rendering problems through a warnings API.
Contents
- Requirements
- Installation
- Configuration
- Quick start
- Inspecting a template
- Recipient data
- Dynamic QR code and barcode values
- Replacing logos and signatures
- Bulk issuance
- Warnings and error handling
- Public API
- Rendering compatibility
- Security and production guidance
- Troubleshooting
- Testing
Requirements
- PHP 8.2 or newer (8.3+ on Laravel 13)
- Laravel 12 or 13 - both are covered by CI on every push, across the lowest and highest dependency set each constraint allows
- The
zipPHP extension (ext-zip) - A writable system temporary directory for Dompdf's font cache
- The PHP extensions required by Dompdf and the selected image formats
- A
gs(Ghostscript) executable onPATH(e.g.brew install ghostscript/apt-get install ghostscript), but only if you callcapture()- no PHP extension required, it's shelled out to directly
Installation
composer require certigniter/laravel-certificate-renderer
Laravel package discovery registers the service provider and Certigniter
facade automatically.
Configuration
Publish the configuration file:
php artisan vendor:publish --tag=certigniter-config
Set the shared Certigniter encryption key in .env:
CERTIGNITER_ENCRYPTION_KEY=your-32-byte-shared-key
This is not Laravel's APP_KEY. It must exactly match the key the file was
encrypted with. A wrong key causes decryption to fail before parsing or
rendering begins.
The published configuration also controls:
- composition of parent-group rotation and opacity;
- the Ghostscript binary used by
capture().
There is nothing to configure for fonts. This package ships none: every family
a certificate uses travels inside the .igniter file (see
Fonts).
Views can be published only when a project genuinely needs to customize the renderer markup:
php artisan vendor:publish --tag=certigniter-views
Prefer the package view unless you are prepared to keep a published copy up to date as new element properties are added.
Quick start
Inject CertificateRenderer, read the encrypted upload, and return the PDF:
use Certigniter\CertificateRenderer\CertificateRenderer; use Illuminate\Http\Request; final class CertificateController { public function render(Request $request, CertificateRenderer $renderer) { $request->validate([ 'template' => ['required', 'file'], ]); $encrypted = $request->file('template')->get(); $pdf = $renderer->renderIgniterToPdf($encrypted); return response($pdf, 200, [ 'Content-Type' => 'application/pdf', 'Content-Disposition' => 'inline; filename="certificate.pdf"', ]); } }
The facade provides the same methods:
use Certigniter\CertificateRenderer\Facades\Certigniter; $pdf = Certigniter::renderIgniterToPdf($encrypted);
Inspecting a template
Parse once before rendering when the web application needs to build a form, CSV template, or image-replacement screen:
$project = $renderer->parseIgniter($encrypted); $metadata = [ 'id' => $project->id, 'title' => $project->title, 'width' => $project->width, 'height' => $project->height, 'unit' => $project->unit, 'recipient_fields' => $project->variableNames(), 'elements' => $project->elementCatalog(), 'replaceable_images' => $project->getElementIds('image'), ];
Element IDs are stable within the template. Store or submit those IDs when the issuer chooses which logo or signature to replace.
elementCatalog() and its intuitive alias getElementIds() return metadata,
not just bare IDs, so a developer can build a useful selection screen:
[
[
'id' => 'signature-uuid',
'type' => 'image',
'label' => 'Director signature',
'visible' => true,
'replaceable' => true,
'parentGroupId' => null,
'position' => [
'x' => 24.0,
'y' => 165.0,
'width' => 42.0,
'height' => 14.0,
'unit' => 'mm',
],
'details' => [
'hasEmbeddedData' => false,
'hasLocalPath' => true,
'originalFilename' => 'director-signature.png',
'fit' => 'contain',
'maskShape' => 'none',
'aiGenerated' => false,
'aiModel' => null,
'aiGeneratedAt' => null,
'aiPromptPreview' => null,
'replacementHint' => 'signature',
],
],
]
The replacement hint is one of background, logo, signature, or image.
aiGenerated/aiModel/aiGeneratedAt/aiPromptPreview describe an
AI-generated image. aiGenerated alone is enough to classify an image as
background, even if it has since been resized below the 90% area threshold
the hint otherwise uses. aiPromptPreview is truncated to 80 characters, like
textPreview and dataPreview elsewhere in this catalog. The catalog also provides
relevant summaries for text, variable text, shapes, QR codes, barcodes, and
groups. It intentionally excludes embedded base64 font and image payloads,
making it safe and lightweight to return as JSON.
parseIgniter() returns a typed Data\CertificateProject; it does not render
a PDF or mutate the uploaded file.
Recipient data
Pass one flat associative array for one certificate:
$recipient = [ 'Recipient Name' => 'Ada Lovelace', 'Certificate ID' => 'CERT-0001', 'Issue Date' => '2026-08-11', ]; $pdf = $renderer->renderIgniterToPdf( $encrypted, recipient: $recipient, );
Certigniter stores fields in two forms, so there are two merge mechanisms:
| Template element | Stored form | Matching behavior |
|---|---|---|
| Variable text | variableName: "Recipient Name" |
Case-insensitive and trimmed |
| "Dynamic value" QR code/barcode | qrType: "dynamic", variableName: "Certificate ID" |
Case-insensitive and trimmed, replaces the whole value |
| Other QR/barcode data | {{Certificate ID}} or <Certificate ID> |
Exact (case-sensitive) token replacement; spaces inside the delimiters are ignored |
Use $project->variableNames() to obtain the distinct fields required by
both mechanisms in template order.
When no recipient is supplied, variable text renders empty, QR/barcode tokens remain unresolved, and a "Dynamic value" barcode is skipped with a warning. That is normally suitable only for structural template previews.
Consistent date formatting
$recipient values are substituted exactly as given: 'Issue Date' => '2026-08-11' and 'Issue Date' => 'Aug 11, 2026' are both valid. If a value
comes from a real date rather than an already-formatted string, format it
with $project->dateFormat first, so every certificate for a project shows
dates in the format its designer chose:
use Certigniter\CertificateRenderer\Support\DateFormatting; $project = $renderer->parseIgniter($encrypted); $recipient = [ 'Recipient Name' => 'Ada Lovelace', 'Issue Date' => DateFormatting::format($issuedAt, $project->dateFormat), ];
$project->dateFormat is an ICU date pattern such as 'MMM d, yyyy', which
is also the default for files that predate the field.
DateFormatting::toPhpFormat() and ::format() translate the supported
patterns into PHP's date() syntax:
| Pattern | Example |
|---|---|
MMM d, yyyy (default) |
Aug 11, 2026 |
MMMM d, yyyy |
August 11, 2026 |
d MMM yyyy |
11 Aug 2026 |
MM/dd/yyyy |
08/11/2026 |
dd/MM/yyyy |
11/08/2026 |
yyyy-MM-dd |
2026-08-11 |
EEEE, MMMM d, yyyy |
Tuesday, August 11, 2026 |
An unrecognized pattern falls back to the default rather than guessing at a translation.
A text element's recipient value that is already an ISO date (yyyy-MM-dd,
e.g. from a CSV import or an HTML <input type="date">) is reformatted
automatically during rendering, using the element's own date format or,
failing that, the project's.
Dynamic QR code and barcode values
A QR code or barcode element's content source (qrType) is one of:
- Custom value (
'custom') - a static string, optionally containing{{token}}/<token>placeholders resolved fromrecipientlike any other QR/barcode data (see above); or - Dynamic value (
'dynamic') - bound to exactly one recipient/CSV column, named inproperties.variableName(and mirrored intodataas{{ variableName }}for older consumers) - the whole payload is replaced by that column's value, matched case-insensitively and trimmed like a text element'svariableName; a recipient with no value for it skips the code with a warning; or - Verification link (
'verification'-'authentication'is a legacy value older projects may still carry, and is accepted identically) - the element intentionally stores nodataat all. Its real payload doesn't exist until the certificate is issued, so it must be supplied by your application at render time - typically a unique verification URL such ashttps://you.example.com/verify/{id}.
Barcodes support the same three content sources. A barcode element
carries the same qrType/variableName properties, so a barcode can encode
a per-recipient column ('dynamic') or an application-supplied verification
code ('verification') exactly like a QR code. Pick a symbology that can
encode the value - Code 128 handles any ASCII verification URL or code;
EAN/UPC/ITF only accept digits, and a value they reject is skipped with a
warning.
Use $project->verificationCodeElementIds() to find every verification QR
code and barcode in a parsed project (a template may have e.g. one of each),
then pass the value you generated in qrCodeOverrides, keyed by element ID.
authenticationQrElementId() still returns just the QR one:
$project = $renderer->parseIgniter($encrypted); $verificationUrl = route('certificate.verify', ['id' => $certificateId]); $qrCodeOverrides = array_fill_keys($project->verificationCodeElementIds(), $verificationUrl); $pdf = $renderer->renderProjectToPdf( $project, recipient: $recipient, qrCodeOverrides: $qrCodeOverrides, );
qrCodeOverrides accepts any QR code or barcode element ID, not only a
verification link. An explicit override always wins over both a static data value and
{{token}}/<token> substitution, so it also works as a direct escape
hatch for a value your application computed rather than one that came from
a recipient record. If a verification-link code has no override
supplied for it, it is skipped with a warning (see below) rather than
encoding an empty or placeholder string into the certificate.
For bulk issuance, each recipient's link is normally unique per row - build
the override map fresh inside the loop, e.g. from a Verification URL
column already present in that row's data, or by minting one from your own
application state per iteration.
Replacing logos and signatures
A template may contain the wrong branding, or an image stored only as a local
path such as /Users/designer/Desktop/signature.png, which a server cannot
read. Supply replacement bytes keyed by the image element ID:
$request->validate([ 'logo' => ['nullable', 'image', 'max:10240'], 'signature' => ['nullable', 'image', 'max:10240'], ]); $imageOverrides = array_filter([ 'logo-element-id' => $request->file('logo') ? base64_encode($request->file('logo')->get()) : null, 'signature-element-id' => $request->file('signature') ? base64_encode($request->file('signature')->get()) : null, ]); $pdf = $renderer->renderIgniterToPdf( $encrypted, recipient: $recipient, imageOverrides: $imageOverrides, );
Raw base64 or a data:image/...;base64,... URI is accepted. The renderer:
- finds the image by element ID;
- clones that render element;
- replaces
imageDataand ignores its original local path; - preserves position, size, fit, alignment, masks, opacity, rotation, and mirroring;
- leaves the parsed project and uploaded
.igniterfile unchanged.
Unknown IDs and IDs belonging to non-image elements are ignored. Validate upload MIME type and size in the host Laravel application before encoding.
Bulk issuance
For bulk work, parse once, then reuse the project and image map:
$project = $renderer->parseIgniter($encrypted); $imageOverrides = [ 'logo-element-id' => base64_encode($request->file('logo')->get()), ]; foreach ($recipients as $index => $recipient) { $pdf = $renderer->renderProjectToPdf( $project, recipient: $recipient, imageOverrides: $imageOverrides, ); $zip->addFromString("certificate-{$index}.pdf", $pdf); foreach ($renderer->warnings() as $warning) { logger()->warning($warning, ['row' => $index]); } }
Parsing once avoids repeatedly unpacking and decoding the same template.
Each renderProjectToPdf() call creates an independent PDF and resets the
warning list.
For large batches, process work in a queue, place limits on template/image uploads, and write PDFs incrementally rather than retaining every PDF in RAM.
Warnings and error handling
Invalid encryption, malformed JSON, or an unrecoverable rendering failure throws an exception. Catch it at the request or queue-job boundary:
try { $pdf = $renderer->renderIgniterToPdf($encrypted, $recipient); } catch (Throwable $error) { report($error); return response()->json([ 'message' => 'The certificate could not be rendered.', ], 422); }
Recoverable element failures do not abort the certificate. Inspect warnings after each render:
foreach ($renderer->warnings() as $warning) { logger()->warning('Certificate element skipped', [ 'warning' => $warning, ]); }
Typical warnings include:
- an image contains only a path from another computer and no replacement was provided;
- a QR/barcode token was not resolved;
- barcode data is invalid for its selected symbology;
- a "Verification link" QR code or barcode had no value supplied for it in
qrCodeOverrides; - a "Dynamic value" QR code or barcode had no data column set, or the recipient had no value for that column.
Public API
renderIgniterToPdf()
renderIgniterToPdf( string $igniterContents, ?array $recipient = null, ?string $encryptionKey = null, array $imageOverrides = [], array $qrCodeOverrides = [], ): string
Convenience entry point that unpacks, parses, merges, overrides images and
QR code/barcode values, and returns PDF bytes. $qrCodeOverrides is keyed
by QR code or barcode element ID. Pass a per-request encryption key only
when intentionally supporting files from a different trusted key domain.
parseIgniter()
parseIgniter(
string $igniterContents,
?string $encryptionKey = null,
): Data\CertificateProject
Unpacks and parses without rendering.
renderProjectToPdf()
renderProjectToPdf( Data\CertificateProject $project, ?array $recipient = null, array $imageOverrides = [], array $qrCodeOverrides = [], ): string
Preferred rendering method after a project has already been inspected.
capture()
capture( string $igniterContents, ?array $recipient = null, ?string $encryptionKey = null, array $imageOverrides = [], array $qrCodeOverrides = [], ?string $outputPath = null, int $resolution = 150, ): string
Renders straight to a PNG preview and writes it to $outputPath (a temp file
when omitted), returning the path written. Use it when your application
imports an .igniter file, to get a thumbnail to display for it. Shells out
to a gs (Ghostscript) binary directly - no imagick PHP extension
involved - configurable via
certigniter.ghostscript_binary / CERTIGNITER_GHOSTSCRIPT_BINARY (see
Requirements); throws a RuntimeException if the binary is
missing or fails.
Two things worth knowing before wiring this into an install/import flow:
- A missing Ghostscript binary shouldn't be fatal to your own install
step. If you call
capture()from a Composer script or similar onboarding hook, catch the exception and treat it as non-fatal - Composer aborts the entireinstall/updateon any non-zero script exit, so a teammate or CI runner without Ghostscript would otherwise be unable to install your app at all. Log a warning and exit0instead. - A real-world
.igniterfile with path-only images will produce an incomplete thumbnail, not an error. Per theimageData-vs-pathdistinction described under Replacing logos and signatures, any image element that only has a localpath(common for files exported before a project embeds its assets) is silently skipped -capture()still returns a PNG, it's just missing that background/logo. Always checkwarnings()right after callingcapture()and surface it (e.g. "this certificate's preview may be missing some images") rather than assuming a returned path means a complete render.
warnings()
warnings(): array
Returns non-fatal warnings from the most recent render call.
Project inspection helpers
$project->variableNames(): array; $project->elementCatalog(?string $type = null): array; $project->getElementIds(?string $type = null): array; $project->authenticationQrElementId(): ?string; $project->verificationCodeElementIds(): array;
getElementIds() is an alias of elementCatalog() and returns the same rich
metadata. Use getElementIds('image') for a logo/signature replacement UI.
authenticationQrElementId() returns the element ID of the project's
"Verification link" QR code, or null if it has none;
verificationCodeElementIds() returns every verification QR code and
barcode - see Dynamic QR code and barcode values.
Rendering compatibility
| Feature | Support |
|---|---|
| CSS hex and legacy ARGB color formats | Yes |
| Static and variable text | Yes |
| Font family, weight, style, alignment | Yes |
| Line height and letter spacing | Yes |
| Underline, strike-through, blurred shadow | Yes |
| Text bottom borders | Yes |
| Rectangles, ellipses, polygons and library (path) shapes | Yes |
| Lines, arrows and elbow connectors | Yes |
| Gradient shape fills | Yes (as fine bands) |
| Blurred drop shadows | Yes (approximated) |
| Per-corner rounded rectangles | Yes |
| Images with contain/fill/cover, crops and content alignment | Yes |
| Circle and rounded-rectangle image masks, with size factors | Yes |
| Horizontal and vertical element mirroring | Yes |
| QR codes, with padding and square or round modules and eyes | Yes |
| Dynamic and verification-link QR codes and barcodes | Yes |
| Code 39, EAN-13, EAN-8, UPC-A, ITF, Codabar and Code 128 barcodes, with caption | Yes |
| Group rotation and opacity composition | Yes, configurable |
Fonts carried inside the .igniter |
Yes |
QR codes and barcodes are encoded deterministically: the same value always produces the same symbol, module for module.
Typography values are converted from 96-DPI CSS pixels to 72-DPI PDF points, so text is set at exactly its designed size.
Fonts
A .igniter file is self-contained: every font family it uses travels inside
it. This package ships no font files of its own and registers exactly what the
file carries.
A family the file names but carries no bytes for renders in Dompdf's own built-in DejaVu Sans. Re-save such a project from Certigniter so its fonts travel with it.
Known limitations
- Dompdf approximates some CSS rotation behavior.
- Dompdf cannot faithfully reproduce gradient-filled text, so the first gradient stop is used as a flat fallback color.
- Dompdf draws no SVG gradients, filters or clip paths. Gradient shape fills are drawn as flat bands at most 0.25 mm apart, and blurred shadows (shape and text) as 48 faint copies spread over the blur's Gaussian (8 for very heavy library shapes). Both read as smooth gradients and soft shadows at print resolution.
- Barcode captions are set in Roboto when the file carries it, otherwise in DejaVu Sans.
- A path-only image from another computer cannot render unless your application supplies an image override.
- The project model is currently single-page/single-sided.
Security and production guidance
- Treat
CERTIGNITER_ENCRYPTION_KEYas a shared secret. Do not commit it. - Validate uploaded file size and MIME type before reading it into memory.
- Do not trust original image paths from uploaded templates. Remote access is disabled and Dompdf is restricted to the font cache directory.
- Uploaded
.igniterfiles are read in memory, never extracted to disk. The reader enforces size limits and rejects any file that is malformed, has unexpected content, or has been tampered with. - Authorize who may render, inspect, or replace certificate assets.
- Escape user-facing metadata when displaying project titles or element names.
- Use queues and execution limits for bulk issuance.
- Record warnings and issuance failures for auditability.
- Use unique, sanitized filenames when creating ZIP archives.
Troubleshooting
The .igniter file cannot be opened
Confirm CERTIGNITER_ENCRYPTION_KEY matches the Certigniter application that
created the file. Clear Laravel's cached configuration after changing .env:
php artisan config:clear
A logo or signature is missing
Inspect the image element. If it has path but no imageData, upload a
replacement and pass it in imageOverrides using that element's ID.
Text is in the wrong typeface or size
Check warnings() and the font's presence in the file: a family the file
carries no bytes for renders in DejaVu Sans, whose metrics differ. See
A font falls back to DejaVu Sans.
A barcode is skipped
Ensure all merge tokens were supplied and the resulting value is valid for
the selected barcode format. A "Dynamic value" barcode also needs its
variableName column in the recipient data, and a "Verification link"
barcode needs a value in qrCodeOverrides. EAN-13, EAN-8, UPC-A and ITF only
encode digits; use Code 128 for a verification URL. Read warnings() for the
element ID and cause.
A font falls back to DejaVu Sans
The file carries no bytes for that family, and the package ships no fonts to fill the gap. Re-save the project from Certigniter so the family is embedded; the renderer then uses those bytes directly. There is no server-side font directory to install into.
An upload is rejected as "not a .igniter file"
The exception message says what the file looks like instead (an older
.igniter format, a saved web page, an empty upload) and what to do. In most
cases, re-export the certificate from Certigniter and upload that file.
Testing
Two suites, both run by CI on every push across Laravel 12 and 13:
composer test # both suites composer test:unit # tests/Unit composer test:integration # tests/Integration composer analyse # PHPStan, level 8
tests/Unit covers every class that works without a booted Laravel
application - parsing, decryption, geometry, colors, merging, QR and barcode
encoding. It runs standalone after composer install.
tests/Integration boots a real Laravel application with
Testbench, which discovers this package's service provider
through the same extra.laravel metadata a host app's package discovery
uses. It covers what only exists inside a framework - config merging and
publishing, the container binding and facade, and the certigniter:: view
namespace - and renders
.igniter fixtures all the way to real PDF bytes, asserting on what landed
on the page: the text that was drawn, the page box, whether the project's
own font travelled into the file.
Fixtures are built in memory by tests/Fixtures/IgniterFixture, not
committed as binaries, so a test reads as the project it is about and the
suite exercises the current container format rather than a snapshot of it.
License
MIT. See LICENSE.