dskripchenko/php-pdf
A PHP package for generating PDF documents, with support for Laravel applications. Build invoices, reports, and other printable documents programmatically through a straightforward API for creating and customizing PDF files.
Install and verify the runtime.
composer require dskripchenko/php-pdf
Pure PHP means no ImageMagick, Ghostscript, or external PDF binaries at runtime, but check composer.json for required extensions — typically mbstring, zlib, gd (images/charts), and openssl (PKCS#7 signing). Enable openssl in production if you plan to sign documents.
Where to look first, in order:
README.md — feature matrix and the canonical "hello world".examples/ — the fastest way to learn the real API; the package ships runnable scripts for HTML, barcode, chart, merge, form, and signing flows.tests/ — fixtures show expected inputs/outputs, edge cases, and how PDF/A and PDF/X modes are asserted.src/ — look for the Document/Pdf entry point, then Page, Html, Barcode, Chart, Form, Signature, and Merge namespaces. Extension interfaces (font resolver, image loader, signer) usually live near these.Minimal first document:
use Dskripchenko\PhpPdf\Document;
$pdf = Document::make('A4'); // or [595.28, 841.89] in points
$page = $pdf->addPage();
$page->text(40, 60, 'Invoice #1001', ['size' => 16]);
$pdf->save(storage_path('app/invoices/1001.pdf'));
First real use case: an HTML invoice. Render a Blade view to a string, feed it to the HTML importer, and write the result to Laravel's storage. This is the highest-leverage feature because it replaces mpdf/TCPDF for the 80% case (invoices, reports, certificates) without adding a dependency.
$html = view('pdf.invoice', ['invoice' => $invoice])->render();
$pdf = Document::make('A4');
$pdf->addPageFromHtml($html, [
'basePath' => public_path(), // resolves relative <img> and CSS
'margin' => [40, 40, 40, 40],
]);
Storage::disk('local')->put("invoices/{$invoice->id}.pdf", $pdf->output());
Start with one page, one font, one image. Once that renders correctly, layer in headers/footers, barcodes, and page breaks.
Wrap generation in a single service, not in controllers. Keep the PDF package behind an application-level interface (InvoicePdfGenerator, ReportPdfGenerator) so a future swap to mpdf or a headless-Chrome sidecar is a one-class change. The package is young (11 stars), so isolate the blast radius.
final class InvoicePdf
{
public function __construct(private Invoice $invoice) {}
public function render(): string
{
$pdf = Document::make('A4');
$pdf->addPageFromHtml(
view('pdf.invoice', ['invoice' => $this->invoice])->render(),
['basePath' => public_path()]
);
return $pdf->output(); // raw bytes
}
}
HTML/CSS templates: treat it as a print layout engine, not a browser. Use a single-column flow, tables for tabular data, explicit @page sizes/margins, and absolute positioning only for headers/footers. Inline critical CSS or embed it in a <style> block; external stylesheets are fragile. Keep font families explicit and reference fonts you have embedded.
Headers, footers, and page numbering. Most pure-PHP engines expose a page callback or a post-render hook. The reliable pattern is to render the body first, count pages, then stamp footers in a second pass (or use the package's page event if provided). Avoid relying on CSS position: fixed for repeating headers unless the HTML importer explicitly documents support.
Barcodes for operational documents. Generate Code128/QR/EAN directly into the page rather than as pre-rendered PNGs — vector output stays crisp and avoids GD. Size by X-dimension and quiet zone, not by pixel width.
$page->barcode('CODE128', $invoice->number, [
'x' => 40, 'y' => 760, 'width' => 180, 'height' => 40,
]);
Charts for reports. Build charts from arrays (labels/series) and embed as vector drawing. For complex dashboards, generate the chart server-side as SVG/PNG and embed the image; the built-in chart API is best for simple bar/line/pie.
Fonts and branding. Register TTF files once (a service provider or a bootstrap in config/pdf.php) and reuse them. Embed and subset fonts for archival output. If you need a corporate font, confirm the license permits embedding and subsetting.
AcroForm workflows. Use forms when downstream systems must fill fields (government forms, onboarding packets). Fill values, then choose:
flatten() for a final, immutable PDF; orNeedAppearances/appearance streams if viewers must render values.Always verify field names from the source PDF (pdftk dump_data_fields or qpdf --json) before filling.
Signing workflow. Signing must be the last write. Load the finished PDF, attach the PKCS#7 signature, optionally add a timestamp, and then persist without further mutation. Keep the certificate and private key outside the repo, load the passphrase from secrets, and log the certificate serial and signing time for auditability.
$pdf = Document::load($unsignedPath);
$pdf->sign([
'certificate' => config('pdf.cert'),
'private_key' => config('pdf.key'),
'password' => config('pdf.key_password'),
'reason' => 'Invoice approval',
'location' => 'Berlin',
'timestamp' => config('pdf.tsa_url'),
]);
$pdf->save($signedPath);
Merge, split, and stamp. Merging is a resource-level operation: concatenate pages, then deduplicate fonts/images if the API exposes it. For batch workflows (statements, claims), merge per-customer documents, stamp a cover page, and stream the result. Read existing PDFs to extract page counts or text before merging so you can build a table of contents.
PDF/A vs PDF/X. Choose the profile at document creation, not after:
Laravel integration tips.
config/pdf.php; publish and cache it.Storage, then notify or expose a signed URL.response()->streamDownload(fn () => print $bytes, 'invoice.pdf', ['Content-Type' => 'application/pdf']) or serve the stored file; don't hold large byte strings in the session.Storage::disk('local')->path(...) for temp files so paths are absolute and consistent.Testing.
pdftotext -layout) rather than binary hashes.qpdf --check on every generated fixture.mutool draw and diff.The HTML importer is a subset, not Chromium. No JavaScript, limited selectors, and unreliable flex/grid support. CSS that works in a browser may silently collapse. Debug by dumping the exact HTML string and rendering it standalone; simplify to tables and block layout when output is wrong.
Coordinate origin is bottom-left in PDF space. Many wrappers expose top-left helpers, but not all methods agree. If content appears mirrored or off-page, check whether the Y coordinate is being flipped. Keep a small helper (mm(), pt()) and use one unit convention everywhere.
Unicode needs the right font. Standard PDF fonts (Helvetica, Times) cover Latin-1 only. For Cyrillic, CJK, emoji, or currency symbols, embed a TTF with those glyphs. Missing glyphs often render as blanks rather than throwing — test with real customer data.
OTF/CFF fonts and variable fonts may not be supported. Prefer static TTF. If embedding fails, convert with fonttools (fonttools varLib.instancer or otf2ttf) before shipping.
Images: downscale before embedding. A 4000px photo decoded into memory can blow the PHP memory limit. Resize to target DPI (150–300) first, prefer JPEG passthrough, and set memory_limit/max_execution_time for large jobs. Remote image URLs are often disabled or unreliable — download to a temp path and pass an absolute path.
Temp files and concurrency. Pure-PHP PDF writers often use a temp directory. Ensure it is writable and unique per request (or per queued job); shared filenames cause race conditions under load. Clean up in a finally.
Signing breaks on any subsequent save. A signature covers a byte range; modifying the document afterward invalidates it. Sign last, copy the signed bytes to storage, and never re-open-and-save. If you need post-sign metadata, use an incremental update that the signer explicitly supports.
Certificate and TSA details matter. Use PEM certificate + private key with openssl_pkcs7_sign under the hood (or the package’s signer). Test with a real timestamp authority; some TSAs require SHA-256 and reject SHA-1. Check certificate chain inclusion and revocation settings for your compliance regime.
PDF/A validation failures are usually one of four things: unembedded fonts, transparency, missing XMP metadata, or encryption. Fix them at generation time — post-processing with external tools defeats the "dependency-free" advantage.
Merge loses more than pages. Outlines, named destinations, AcroForm fields, attachments, and page labels are frequently dropped or collide. Flatten forms before merging unless you have a specific reason to preserve them. If bookmarks matter, rebuild them after merge.
Reading third-party PDFs is the riskiest operation. Encrypted files, cross-reference streams, and malformed producers can fail. Normalize incoming files with qpdf in a preprocessing step if you control the pipeline; otherwise wrap reads in try/catch and fall back to an error state rather than a 500.
Encrypted PDFs: distinguish owner-password (permissions only) from user-password (content encrypted). Many readers can open owner-password files; user-password files need the password. Don’t assume the package handles both.
Barcodes need quiet zones and correct X-dimension. A barcode that scans on screen can fail on a laser scanner if it is scaled non-uniformly or cropped. Render at final size, keep at least 10× the module width as quiet zone, and validate with a real scanner or zbarimg on a rendered page.
Chart fonts and colors. Charts may use a separate default font from body text; set it explicitly. For print, convert RGB chart colors to CMYK-safe equivalents, or accept color shift.
Configuration caching in Laravel. If you publish config/pdf.php, remember php artisan config:cache in deploy. Paths computed at runtime (e.g., storage_path() inside the config file) can go stale; prefer resolving paths in a service, not in config.
Package maturity. At 11 stars and a recent release, the API may shift. Pin an exact version in composer.json, read the changelog before upgrading, and keep the wrapper interface thin. Expect to read source for undocumented behavior — the tests are often more current than the README.
Debugging toolkit. Keep these installed locally: qpdf --check, qpdf --qdf --object-streams=disable in.pdf out.pdf (readable PDF for inspection), mutool draw -o page-%d.png file.pdf (visual check), pdftotext -layout (content check), pdfinfo (metadata/page size), veraPDF (PDF/A), and zbarimg (barcodes). They turn "the PDF is broken" into a specific, fixable defect.
Extension points to look for. Custom font resolvers (loading fonts from storage/S3), image loaders (auth-protected URLs), barcode/QR renderers, chart drivers, HTML node handlers, and signer implementations. These are the seams for adapting the package to Laravel-specific infrastructure without forking.
How can I help you explore Laravel packages today?