certigniter / laravel-certificate-renderer
Decrypt, parse, and render Certigniter .igniter certificate files as PDFs from any Laravel app.
Package info
github.com/Muchwat/certigniter-renderer-laravel
pkg:composer/certigniter/laravel-certificate-renderer
Requires
- php: ^8.2
- barryvdh/laravel-dompdf: ^2.0|^3.0
- illuminate/support: ^11.0|^12.0|^13.0
- picqer/php-barcode-generator: ^3.0
- simplesoftwareio/simple-qrcode: ^4.2
Requires (Dev)
- phpunit/phpunit: ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Render encrypted Certigniter .igniter certificate templates as PDFs in a
Laravel application—without Flutter, the desktop application, or a browser.
The package can:
- decrypt 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 values (e.g. unique verification links) at issue time;
- 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 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
- Laravel 11 or 12
- A writable system temporary directory for Dompdf's font cache
- The PHP extensions required by Dompdf, Simple QR Code, 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
For local package development, use a path repository instead:
{
"repositories": [
{
"type": "path",
"url": "packages/certigniter/laravel-certificate-renderer",
"options": { "symlink": true }
}
]
}
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 encryptionKey
used by the Certigniter application that created the file. A wrong key causes
decryption to fail before parsing or rendering begins.
The published configuration also controls:
- bundled font-family mappings;
- the fallback font;
- composition of parent-group rotation and opacity.
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 maintain rendering parity when 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',
'replacementHint' => 'signature',
],
],
]
The replacement hint is one of background, logo, signature, or image.
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, );
There are two merge mechanisms because that is how Certigniter stores fields:
| Template element | Stored form | Matching behavior |
|---|---|---|
| Variable text | variableName: "Recipient Name" |
Case-insensitive and trimmed |
| QR/barcode data | {{Certificate ID}} or <Certificate ID> |
Exact token replacement |
Use $project->variableNames() to obtain the distinct fields required by
both mechanisms in template order.
When no recipient is supplied, variable text renders empty and QR/barcode tokens remain unresolved. 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, and this
package has no opinion on which. If your own values come from a real date
(rather than an already-formatted string, e.g. from a CSV import or an
HTML <input type="date">, which always yields ISO yyyy-MM-dd), format
them with $project->dateFormat first so every certificate for a project
shows dates the same way its designer chose in Design Studio's date-format
picker, regardless of how each certificate was issued:
use Certigniter\CertificateRenderer\Support\DateFormatting; $project = $renderer->parseIgniter($encrypted); $recipient = [ 'Recipient Name' => 'Ada Lovelace', 'Issue Date' => DateFormatting::format($issuedAt, $project->dateFormat), ];
$project->dateFormat is a Dart/ICU-style pattern (e.g. 'MMM d, yyyy') -
the same syntax the picker itself uses - and defaults to 'MMM d, yyyy'
for any .igniter file saved before this field existed.
DateFormatting::toPhpFormat() and ::format() translate a small, fixed
set of patterns (exactly the ones the picker offers) into PHP's date()
syntax; an unrecognized pattern falls back to the default rather than
guessing at a translation.
Dynamic QR code values
A QR element's Design Studio "Content source" is either:
- Custom value - a static string, optionally containing
{{token}}/<token>placeholders resolved fromrecipientlike any other QR/barcode data (see above); or - Authentication link - the element intentionally stores no
dataat 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}. A project has at most one of these (the Design Studio enforces it).
Use $project->authenticationQrElementId() to find whether (and where)
a parsed project has one, then pass the value you generated in
qrCodeOverrides, keyed by that element ID:
$project = $renderer->parseIgniter($encrypted); $qrElementId = $project->authenticationQrElementId(); // null if the template has none $qrCodeOverrides = $qrElementId !== null ? [$qrElementId => route('certificate.verify', ['id' => $certificateId])] : []; $pdf = $renderer->renderProjectToPdf( $project, recipient: $recipient, qrCodeOverrides: $qrCodeOverrides, );
qrCodeOverrides accepts any qrcode element ID, not only an authentication
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 an authentication-link QR 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
Templates created elsewhere may contain the wrong branding or a local path
such as /Users/designer/Desktop/signature.png. Server-side code cannot read
that foreign path. 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, decrypt and 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 decrypting 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;
- an "Authentication link" QR code had no value supplied for it in
qrCodeOverrides.
Public API
renderIgniterToPdf()
renderIgniterToPdf( string $encryptedIgniterContent, ?array $recipient = null, ?string $encryptionKey = null, array $imageOverrides = [], array $qrCodeOverrides = [], ): string
Convenience entry point that decrypts, parses, merges, overrides images and QR code values, and returns PDF bytes. Pass a per-request encryption key only when intentionally supporting files from a different trusted key domain.
parseIgniter()
parseIgniter(
string $encryptedIgniterContent,
?string $encryptionKey = null,
): Data\CertificateProject
Decrypts 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 $encryptedIgniterContent, ?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. This is the method a host app
calls at the moment a user installs/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. This package's owncertificate:snapshotArtisan command in the parent app does exactly that (warns, still exits0). - 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;
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
"Authentication link" QR code, or null if it has none - see
Dynamic QR code values.
Rendering compatibility
| Feature | Support |
|---|---|
| Current CSS and legacy Flutter color formats | Yes |
| Static and variable text | Yes |
| Font family, weight, style, alignment | Yes |
| Line height and letter spacing | Yes |
| Underline, strike-through, shadow | Yes |
| Text bottom borders | Yes |
| Rectangle and four-sided polygon shapes | Yes |
| Per-corner rounded rectangles | Yes |
| Images with contain/fill and content alignment | Yes |
| Circle and rounded-rectangle image masks | Yes |
| Horizontal and vertical element mirroring | Yes |
| QR codes | Yes |
| Code39, EAN-13, EAN-8, UPC-A, ITF, Codabar, Code128 | Yes |
| Group rotation and opacity composition | Yes, configurable |
| Embedded project fonts | Yes |
| Bundled Certigniter font families | Yes |
Typography values are converted from Certigniter's 96-DPI canvas pixels to 72-DPI PDF points. This prevents the approximately 1.33Ă— text enlargement seen in older exporters and keeps titles aligned with the Design Studio.
Current projects may contain embedded_fonts; these are registered for the
render. For projects that only store a font-family name, the package uses its
bundled Playfair Display, Cormorant Garamond, Cinzel, Roboto, and Montserrat
files, then falls back to Inter for unavailable system fonts.
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.
- A path-only image from another computer cannot render unless the host app 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 package font/cache directories.
- 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 decrypted
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 a different size from the editor
Update to the latest package revision. Current versions convert canvas pixels
to PDF points. Also verify the template's font is embedded or included in
config/certigniter.php.
A barcode is skipped
Ensure all merge tokens were supplied and the resulting value is valid for
the selected barcode format. Read warnings() for the element ID and cause.
A custom font falls back to Inter
The server needs actual font bytes. Use a template with embedded_fonts, add
the licensed font files to a maintained package customization, or choose a
bundled family.
Testing
This package ships its own standalone PHPUnit suite (tests/Unit) for every
class that works without a booted Laravel application - ColorConverter,
RecipientMerge, Encryption, GroupComposer, ShapeRenderer,
CertificateProject, DesignElement, FontRegistrar, and BarcodeRenderer.
It runs standalone after composer install, with no Laravel app required -
this is what a composer require install outside the Certigniter monorepo
gets to verify its install:
vendor/bin/phpunit
CertificateRenderer, CertificateRendererServiceProvider, and
QrCodeRenderer need a real Laravel container (view resolution, the
simple-qrcode facade binding) and are exercised instead by this repository's
own Laravel host app, via a Pest integration suite with a real encrypted
fixture and tests for single rendering, bulk rendering, image overrides,
typography, shapes, masks, mirrors, fonts, QR codes, and barcodes:
php artisan test tests/Feature/Certigniter
Run the host endpoint tests as well when changing upload or controller logic:
php artisan test tests/Feature/CertificateControllerTest.php
License
MIT. See LICENSE.