spraed/pdf-generator-bundle
Symfony bundle that turns Twig and HTML templates into PDFs using wkhtmltopdf, exposing a lightweight, configurable service API so you can render documents and return them as inline or downloadable responses with minimal setup in any Symfony project.
Reality check first: this is a Symfony bundle, not a Laravel package. It ships a Bundle class, a DI extension, and an XML service definition — none of which Laravel's container will autoload. composer require spraed/pdf-generator-bundle inside a Laravel 9+ app will fight you on symfony/framework-bundle version constraints (the 2020 release caps at Symfony 5). Treat it as reference implementation for invoking Flying Saucer, not a drop-in dependency.
Three things to verify before writing any code:
PATH. Flying Saucer is a Java library; the bundle shells out to a JVM per render. java -version must succeed on every box, including queue workers and CI.DependencyInjection/Configuration.php. That file is the single source of truth for the config keys (binary, jar, tmp_dir or similar — the surface is small). If the keys don't match the README example, trust the Configuration class.Resources/java/. If it isn't shipped, you must build or download the Flying Saucer + iText jar yourself — and that upstream project died with Google Code, so plan for an archived copy.First use case: a server-side invoice or statement where you already have HTML/CSS and need pixel-stable A4 output with real page breaks, headers, and footers. Flying Saucer honours CSS 2.1 paged media (@page, size: A4, @bottom-center { content: counter(page) }), which is exactly the thing Dompdf and mPDF fumble. Render a Blade view to a string, hand it to the Java process, return a Content-Type: application/pdf response.
For a Laravel codebase, the cleanest first step is to skip the bundle and wrap the jar yourself with the Process component:
$html = view('invoices.pdf', $data)->render();
$tmp = tempnam(sys_get_temp_dir(), 'pdf').'.html';
file_put_contents($tmp, $html);
Process::timeout(60)
->run(['java', '-Djava.awt.headless=true', '-jar', config('services.pdfgen.jar'), $tmp, $out]);
return response()->file($out)->deleteFileAfterSend();
Render-to-string, then hand off. Never let the Java process fetch assets. Inline all CSS in a <style> block and convert images to absolute file:// paths or data: URIs. Your Blade view for PDF should have zero asset()/mix()/Vite calls — those produce URLs the JVM cannot resolve and you get silent missing logos.
Template split. Keep resources/views/pdf/*.blade.php separate from the web views. PDF views need table-based or float-based layout, explicit width in mm/pt, and a UTF-8 meta tag. Reusing web templates is the fastest route to a broken PDF.
Two output modes. Day-to-day you need exactly two: stream to browser (response()->file() / the bundle's generatePDF($html) cast to a Response) and persist to storage (Storage::disk('local')->put(...), then attach to a Mailable or queue a job). Model this as one small service class with render(string $html): string and renderToFile(string $html, string $path): void so controllers stay thin.
Merging documents. The bundle's selling point is generatePDFs(array $htmls) producing one concatenated PDF — useful for a cover page + invoice + terms in a single download. If you go the raw-jar route, either concatenate the HTML into one document with page-break-before: always between sections (simplest, preserves headers/footers) or merge with a separate library.
Queue it. A JVM cold start is roughly 1–3 seconds per document. For anything user-triggered at scale, dispatch a job, write to storage, and serve via signed URL. This also caps memory pressure and gives you retries.
Config. Put the jar path and Java binary in config/services.php (or a dedicated config/pdf.php), read from .env, and fail loudly at boot if is_file($jar) is false. A missing jar should be a deployment error, not a runtime 500 at 2 a.m.
calc(), no CSS variables, no gap. If your design relies on Tailwind flex utilities, expect everything to stack. This is the single biggest source of "why does it look wrong" tickets.font-family against installed system fonts and any @font-face you point at a real file:// path. A font that renders in the browser and not in the PDF means it isn't installed on the server. Keep a curated set and fc-cache -f after adding.<meta charset="utf-8"> and ensure the HTML is written as UTF-8 bytes. Otherwise curly quotes and accented names arrive as ?. Watch your Blade template file encodings too — Windows line endings plus BOM cause leading blank pages.tmp_dir. The bundle does not always clean up on exception. Set an app-specific tmp_dir, sweep it on a schedule, and never share a tmp_dir across parallel queue workers without unique filenames — concurrent renders in the same directory produce corrupt or mismatched output.java -jar ... command by hand; the JVM will tell you the real story (unresolved URI, missing font, XHTML parse error, OutOfMemoryError).<br>, <img>, <hr>) and unescaped & in text. Blade output from user data must be escaped — <br> for newlines, & in addresses. Run templates through a strict validator if you keep getting parse exceptions.-Xmx256m via the binary/options config and throttle the queue.storage/app/bin, and document the exact version. Don't rely on composer update to keep this working.How can I help you explore Laravel packages today?