PHPackages                             tklaversma/flexpdf - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [PDF &amp; Document Generation](/categories/documents)
4. /
5. tklaversma/flexpdf

ActiveLibrary[PDF &amp; Document Generation](/categories/documents)

tklaversma/flexpdf
==================

HTML to PDF for Laravel with real CSS3 support: flexbox, grid, transforms and font embedding, with no binary dependencies.

v0.1.0(today)10MITPHPPHP ^8.4CI passing

Since Aug 24Pushed today1 watchersCompare

[ Source](https://github.com/Tklaversma/FlexPDF)[ Packagist](https://packagist.org/packages/tklaversma/flexpdf)[ Docs](https://github.com/tklaversma/flexpdf)[ RSS](/packages/tklaversma-flexpdf/feed)WikiDiscussions main Synced today

READMEChangelog (3)Dependencies (12)Versions (2)Used By (0)

FlexPDF
=======

[](#flexpdf)

[![Latest Version on Packagist](https://camo.githubusercontent.com/7ed398c83446ea404d94b6602dc42e16f6657e1ee231a44080d787c09e04e6e3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f746b6c61766572736d612f666c65787064662e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/tklaversma/flexpdf)[![GitHub Tests Action Status](https://github.com/tklaversma/flexpdf/actions/workflows/run-tests.yml/badge.svg)](https://github.com/tklaversma/flexpdf/actions?query=workflow%3Arun-tests+branch%3Amain)[![Total Downloads](https://camo.githubusercontent.com/62e33387535d953f0b1f30bcebfc0c25b5106356f780ff23d0073cafd1df83bc/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f746b6c61766572736d612f666c65787064662e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/tklaversma/flexpdf)

**HTML to PDF for Laravel, in pure PHP, with a modern CSS layout engine.**

Write your PDF templates as Blade views with flexbox, grid, custom fonts and real typography. Render them in the same PHP process as the rest of your request. No headless browser, no `wkhtmltopdf`, no Gotenberg container, no Node.js, no binaries to ship.

```
use FlexPDF\Facades\Pdf;

Pdf::view('invoices.show', ['invoice' => $invoice])->download('invoice.pdf');
```

Note

**FlexPDF is in beta.** The engine is heavily tested and renders real production templates today, but the API may still shift before 1.0. Anything marked 🚧 below is not a dead end: it is on the list and being worked on. See [ROADMAP.md](ROADMAP.md).

Why FlexPDF
-----------

[](#why-flexpdf)

Every HTML-to-PDF approach in PHP sits somewhere on one trade-off: layout quality against deployment weight.

- **Browser-based tools** (Browsershot, Snappy, Gotenberg) produce excellent output, at the cost of a Chrome or wkhtmltopdf process next to your app: something to install, patch, monitor and scale, with an out-of-process failure mode.
- **Classic pure-PHP libraries** (dompdf, mpdf, TCPDF) deploy with `composer require` and nothing else, but their CSS support predates flexbox. Layouts get built out of tables and absolute positioning, and the template you wrote is not the page you get.

FlexPDF keeps the pure-PHP deployment model and closes the layout gap with an engine written from scratch: it parses your HTML and CSS, runs the real flexbox, grid and table algorithms to give every box its geometry, cuts the result into pages, and writes the PDF itself, fonts subset and embedded. Not a browser behind an API, and not HTML mapped onto PDF tables.

So flexbox, grid, container queries, `@page`, custom properties and modern color functions all work, and the CSS you write for the screen is close to the CSS you write for paper. And because everything happens in your PHP process, a render is one function call: no temp files, no shelling out, nothing to keep running.

Two things about the output are worth knowing up front:

- **Same input, same file. Always.** Render the same HTML twice and you get the exact same PDF, byte for byte: today, next week, on your laptop, on the server. Most tools cannot do this, because they put timestamps and random IDs in the file. Here you can write a test that says "the output must equal this saved file", cache by content hash, and re-render last year's invoice into a provably identical document. (The one exception is an encrypted document, because encryption keys must be random.)
- **Bad input cannot hang your server.** A broken or hostile document can send a layout engine into an endless loop, and this one runs inside your PHP process. So every render has hard limits: pages, nesting depth, and a wall-clock timeout. Cross one and you get a normal exception to catch, not a request that eats CPU until something kills it.

What CSS is supported
---------------------

[](#what-css-is-supported)

The rule of thumb: **the CSS you write for a modern browser mostly works here.** The tables give the honest picture, gaps included: ✅ works today, ⚠️ partial, 🚧 not yet, in development.

**Layouts**

FeatureNotes✅Flexbox`gap`, `order`, reverse directions, auto margins, baseline alignment✅Grid`fr`, `minmax()`, `repeat()`, `auto-fill` / `auto-fit`, template areas, spans🚧Subgrid✅Tables`colspan` / `rowspan`, `border-collapse`; `` and `` repeat on every page a long table crosses✅Floatswith real text wrap around them✅Positioning`relative`, `absolute`, `fixed`✅Multi-columnbalanced✅Box modelmargin collapsing, `box-sizing`, percentage padding and margins, `aspect-ratio`, `overflow: hidden`**Values, colors and conditional rules**

FeatureNotes✅CSS variables`var()`, also inside `calc()`✅Math functions`calc()`, `min()`, `max()`, `clamp()`✅Viewport units`vw` / `vh` resolve against the page size✅Modern colors`hsl()`, `oklch()`, `color-mix()`, hex with alpha✅Conditional rules`@media print`, `@supports`, `@container` queries, `@scope`, `@import`⚠️`@layer`rules inside apply; layer ordering is ignored✅Generated content`::before` / `::after`, counters**Pages and breaks**

FeatureNotes✅`@page`paper size and margins from CSS, `:first`, named pages✅Forced breaks`break-before` / `break-after: page`✅Keeping content together`break-inside: avoid`, `orphans`, `widows`⚠️Named page with another widththe sheet size is right, line wrapping still uses the document's width**Graphics and decoration**

FeatureNotes✅ImagesPNG, JPEG, GIF, WebP, `data:` URIs, `object-fit`✅SVGinline and as ``, full path grammar✅Transformstranslate, rotate, scale, matrix✅Effects`opacity`, `mix-blend-mode`🚧`filter``grayscale()`, `blur()` and the rest are dropped for now✅`linear-gradient()`🚧`radial-gradient()`renders as no background for now✅Borders and shadows`border-radius`, `box-shadow`⚠️`text-shadow`offset and color drawn, blur radius ignoredAnything the engine does not know is skipped the way a browser skips unknown CSS: that declaration is dropped and the rest of your stylesheet still applies. What is missing beyond CSS is under [Current limitations](#current-limitations).

Requirements
------------

[](#requirements)

- PHP 8.4+
- Laravel 11+
- The `dom`, `gd` and `zlib` extensions (all commonly enabled)

Installation
------------

[](#installation)

```
composer require tklaversma/flexpdf
```

Optionally publish the config file:

```
php artisan vendor:publish --tag="flexpdf-config"
```

This gives you `config/flexpdf.php` with defaults for page size, margins, fonts, metadata, safety limits, remote images, tagging, PDF/A and encryption. Everything in it can also be set per document on the builder.

Quick start
-----------

[](#quick-start)

The most common case: a controller that turns a Blade view into a PDF the browser downloads.

```
use FlexPDF\Facades\Pdf;

class InvoiceController extends Controller
{
    public function download(Invoice $invoice)
    {
        return Pdf::view('invoices.show', ['invoice' => $invoice])
            ->download("invoice-{$invoice->number}.pdf");
    }
}
```

Every call follows the same shape: **where the HTML comes from, then what to do with the PDF.** Anything in between (page size, fonts, metadata, and so on) is optional and covered in the sections below.

**Where the HTML comes from.** Pick one:

```
Pdf::view('invoices.show', ['invoice' => $invoice])   // a Blade view, like view()
Pdf::html('Hello')                           // an HTML string you already have
Pdf::loadFile(resource_path('templates/report.html')) // an HTML file on disk
```

**What to do with the PDF.** Pick one:

```
->download('invoice.pdf')                        // browser shows a save dialog
->inline('invoice.pdf')                          // opens right in the browser tab
->save(storage_path('app/invoices/invoice.pdf')) // write to disk
->output()                                       // the raw bytes, when you need them yourself
```

The `download()` and `inline()` functions return responses, so you `return` them straight from a controller. The function `stream()` also exists and behaves like `inline()`, as a streamed response. The function `save()` writes the file and returns the page count.

The function `output()` is for everything else Laravel does with bytes, like storing on a disk or attaching to a mail:

```
// Store on disk
Storage::disk('s3')->put(
    'invoices/invoice.pdf',
    Pdf::view('invoices.show', $data)->output()
);

// Use in a Mailable
$this->attachData(
    Pdf::view('invoices.show', $data)->output(),
    'invoice.pdf',
    ['mime' => 'application/pdf']
);
```

Not using Laravel? The engine has no Laravel dependency: `FlexPDF\Engine\Html::make($html)` exposes the same capabilities directly.

Examples
--------

[](#examples)

[`examples/`](examples/) holds nine complete documents, each a Blade template plus the exact builder chain that renders it: an invoice, a bank statement, an annual report, a slide deck, a product catalog, a CSS showcase, a fillable form, a PDF/A-3 e-invoice with an attached XML, and an encrypted document. They run from a clone with `composer install && php examples/render.php`, and each one is meant to be copied into your app as a starting point. The [examples README](examples/README.md) says what every document shows.

Page setup, headers and footers
-------------------------------

[](#page-setup-headers-and-footers)

```
Pdf::view('report')
    ->page('a4') // a3, a4, a5, letter, legal, tabloid, or [w, h] in points
    ->landscape()
    ->margins(40, 30) // top/bottom, left/right (CSS shorthand order)
    ->header('Quarterly report')
    ->footer('Page {page} of {pages}')
    ->inline('report.pdf');
```

`@page` in your CSS works too, including named pages, so a document can carry its own geometry:

```
@page { size: A4; margin: 20mm }
@page cover { size: A4 landscape; margin: 0 }
@page :first { margin-top: 60pt }

.cover { page: cover }
```

**Headers and footers can differ per page.** Pass a callable: it receives the 1-based page number and the total, and returns that page's markup or `null`for none. The count is real, because pagination finishes before the first header is drawn, so "last page only" is as easy as "first":

```
Pdf::view('report')
    ->header(fn (int $page): ?string => $page === 1
        ? null
        : 'Quarterly report')
    ->footer(fn (int $page, int $total): ?string => $page === $total
        ? 'End of report'
        : '{page} / {pages}')
    ->inline('report.pdf');
```

Fonts and writing systems
-------------------------

[](#fonts-and-writing-systems)

**Built in, zero setup.** The classic PDF fonts Helvetica, Times and Courier, plus DejaVu Sans, which ships with the package. Common names map onto them, so `font-family: Arial`, `sans-serif`, `serif` or `monospace` just work.

If your text contains a character the classic fonts cannot draw (ą, ć, ß, Greek, Cyrillic, Hebrew, Arabic), it is drawn from DejaVu Sans automatically instead of coming out as `?`. Nothing to configure.

**Your own fonts.** Point the config at your TTF files:

```
'fonts' => [
    'Inter' => [
        'regular'     => resource_path('fonts/Inter-Regular.ttf'),
        'bold'        => resource_path('fonts/Inter-Bold.ttf'),
        'italic'      => resource_path('fonts/Inter-Italic.ttf'),
        'bold-italic' => resource_path('fonts/Inter-BoldItalic.ttf'),
    ],
],
```

then use `font-family: Inter` in your CSS. Only `regular` is required: a missing bold or italic is generated from it, like a browser does. Fonts can also be registered for a single document with `->font('Inter', $paths)`, or declared in the CSS itself with `@font-face`.

Embedding stays small: only the characters you actually use go into the PDF, so a 742 KB font typically adds around 54 KB to the document.

**Writing systems.** Supported out of the box:

- **Latin**, so every language written in it: English, Dutch, German, French, Polish, Czech, Turkish, Vietnamese, ...
- **Greek**
- **Cyrillic**: Russian, Ukrainian, Bulgarian, Serbian, ...
- **Hebrew** and **Arabic**: right-to-left just works, Arabic letters connect correctly, and the text stays selectable and searchable in the PDF.

Not supported: CJK (Chinese, Japanese, Korean), Indic scripts such as Devanagari (Hindi), Thai, and Khmer.

The typography details are handled too: hyphenation, justification, kerning, ligatures and small caps.

If a document asks for a font that is not there, it still renders (in a fallback font) and `->fontReport()` tells you afterwards what was missing. Prefer failing loudly? `->strictFonts()` throws instead.

What the PDF itself can do
--------------------------

[](#what-the-pdf-itself-can-do)

The output is not just pictures of pages:

- **Selectable, searchable text**, in every script the engine sets
- **Links**: external URLs and internal `#anchor` jumps become real annotations
- **Bookmarks**: the heading structure becomes the reader's outline panel
- **Fillable forms**: ``, `` and `` become AcroForm fields (text, password, checkbox, radio, combo and list boxes)
- **Metadata**: title, author, subject, keywords, creator, producer

```
Pdf::view('report')
    ->metadata(['title' => 'Q3 Report', 'author' => 'Acme B.V.'])
    ->initialView('FitH', page: 1)     // how the reader opens the document
    ->pageMode('UseOutlines')          // with the bookmarks panel showing
    ->save($path);
```

### Accessible and archival PDF

[](#accessible-and-archival-pdf)

```
Pdf::view('invoices.show', $data)
    ->tagged(lang: 'en')   // tagged PDF: a real structure tree and /Lang
    ->pdfa()               // PDF/A-3 (ISO 19005-3), level B, or 'A'
    ->pdfua()              // claim PDF/UA-1 (ISO 14289-1) as well
    ->save($path);
```

Tagging gives every piece of content a role and a reading order, which is what screen readers consume. PDF/A adds the color profile, XMP metadata and font rules an archival file needs; the output validates against veraPDF. Claims are honest: asking for level A or PDF/UA without a structure tree and language is refused with an exception rather than written as an empty promise.

### Encryption

[](#encryption)

```
Pdf::view('payslip', $data)
    ->encrypt('user-password', allow: ['print'])
    ->download('payslip.pdf');
```

AES-256 (PDF 2.0, revision 6), with the standard permission set: `print`, `copy`, `modify`, `annotate`, `fill_forms`, `assemble`, `print_high_quality`. Older, broken revisions are deliberately not offered. Output opens in Acrobat 9+, macOS Preview, pdf.js, pdfium and Ghostscript.

### File attachments and e-invoicing

[](#file-attachments-and-e-invoicing)

A file can travel inside the document, which is what PDF/A-3 exists for and what Factur-X and ZUGFeRD e-invoicing are built on:

```
Pdf::view('invoices.show', $data)
    ->pdfa()
    ->attach('factur-x.xml', $xml, 'text/xml', 'Invoice data', 'Data')
    ->save($path);
```

The attachment is written as an **associated file**: the catalog's `/AF` array names it and its `/AFRelationship` (`Source`, `Data`, `Alternative`, `Supplement`, `Unspecified`) says what it is to the document, which is the pair an e-invoice consumer looks for. Attachments are encrypted along with everything else when the document is.

Rendering untrusted HTML
------------------------

[](#rendering-untrusted-html)

**The supported input is a template you control.** If you render HTML that users influence, read this section.

Layout is a fixpoint computation, and some inputs never converge. Without ceilings, a few KB of hostile HTML consumes unbounded CPU. The limits are therefore **security controls, not tuning knobs**:

```
'limits' => [
    'max_pages'          => 2000,
    'max_depth'          => 64,
    'max_length'         => 200000.0,
    'max_font_size'      => 2000.0,
    'timeout_seconds'    => 30.0,
    'max_gradient_stops' => 500000,
    'max_image_bytes'    => 200_000_000,
],
```

A render that exceeds them throws a named exception (`LayoutTimeoutException`, `PageLimitExceededException`, `GradientLimitExceededException`) rather than returning a document quietly missing its tail. Override the wall clock per render with `->timeout($seconds)`. `max_image_bytes` is the one ceiling that drops rather than throws: it bounds what a raster picture becomes once decoded (width times height times four, read from the header first, so a small file that decodes to gigabytes never gets the chance), and a picture over it draws as a missing image, the same as a remote one over `remote_images.max_bytes`.

**File access is scoped.** `base_path` is the only directory a document can read: ``, `@font-face src` and stylesheet hrefs resolve against it and are refused outside it, symlinks followed. With no base path, no file is reachable at all.

**The network is not touched unless you turn it on.** Remote images are off by default, and enabling them requires naming the hosts:

```
'remote_images' => [
    'enabled'       => true,
    'allowed_hosts' => ['cdn.example.com'],
    'max_bytes'     => 2_000_000,
    'timeout'       => 5.0,
],
```

Fetches are `https` only, exact host match, private and loopback addresses refused, no redirects, body size capped while reading, bytes sniffed rather than the content type believed. **A remote stylesheet or font is never fetched**: a stylesheet is a second document, and a font is glyph data copied into your output.

Still, rendering is CPU work in your process. If the input is not yours, put it behind a queue.

Performance
-----------

[](#performance)

Real templates, rendered on a laptop, single process:

DocumentPagesTimeOutputInvoice (flex layout, custom font, SVG logo)1~180 ms49 KBBank statement (tables across pages)3~330 ms43 KBReport (charts, grid, images)5~320 ms245 KBPresentation (full-bleed pages)8~190 ms466 KBYour numbers will vary with content; the point is the order of magnitude. A typical business document is a few hundred milliseconds, in-process, with no container or browser to keep warm.

Current limitations
-------------------

[](#current-limitations)

The CSS gaps are marked 🚧 and ⚠️ in the tables under [What CSS is supported](#what-css-is-supported). Beyond those, not here yet:

- **Writing systems**: no Chinese, Japanese, Korean, Indic scripts, Thai or Khmer, and no vertical writing or ruby. See [Fonts and writing systems](#fonts-and-writing-systems).

And two things that are **by design and will stay this way**:

- **An encrypted document is not byte-reproducible.** Key material is random per render, as it should be. Everything else in the writer is deterministic.
- **No JavaScript in the output.** A PDF that executes code is an attack surface this package chooses not to have.

Roadmap
-------

[](#roadmap)

FlexPDF is in active development. Everything marked 🚧 or ⚠️ on this page is collected in [ROADMAP.md](ROADMAP.md), with what happens today until each item lands. No dates and no fixed order: what real documents run into first gets built first.

Testing
-------

[](#testing)

```
composer test          # Pest, the Laravel integration
composer test-engine   # the 9 engine suites, 596 tests
composer test-all      # both
```

The engine suites verify output by parsing the generated PDFs back, and the rasterizing checks shell out to Python. To run them locally:

```
python3 -m venv .venv
.venv/bin/pip install pypdf pypdfium2 numpy pillow fonttools

# macOS
brew install --cask font-dejavu
# Debian/Ubuntu
sudo apt-get install fonts-dejavu-core
```

None of this is needed to *use* the package, only to run the engine suites.

There is also a fuzzer, which has found more bugs than every hand-written test combined:

```
php tests/Engine/fuzz.php
```

It generates random documents and asserts invariants: finite geometry, no negative sizes, nothing painting past a page edge, no content lost, pagination proportional to content, and PDFs that parse back.

Credits
-------

[](#credits)

- [Tim Klaversma](https://github.com/tklaversma)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 83.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/2269283?v=4)[Tim Klaversma](/maintainers/Tklaversma)[@Tklaversma](https://github.com/Tklaversma)

---

Top Contributors

[![Tklaversma](https://avatars.githubusercontent.com/u/2269283?v=4)](https://github.com/Tklaversma "Tklaversma (5 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

css-griddompdf-alternativeflexboxhtml-to-pdflaravellaravel-packagepdfphplaravelpdfcssgridCSS3html-to-pdfflexboxtransformsflexpdf

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/tklaversma-flexpdf/health.svg)

```
[![Health](https://phpackages.com/badges/tklaversma-flexpdf/health.svg)](https://phpackages.com/packages/tklaversma-flexpdf)
```

###  Alternatives

[spatie/laravel-permission

Permission handling for Laravel 12 and up

13.0k107.5M1.6k](/packages/spatie-laravel-permission)[spatie/laravel-pdf

Create PDFs in Laravel apps

1.0k5.4M50](/packages/spatie-laravel-pdf)[dedoc/scramble

Automatic generation of API documentation for Laravel applications.

2.2k12.6M141](/packages/dedoc-scramble)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

818355.4k3](/packages/defstudio-telegraph)[rawilk/profile-filament-plugin

Profile &amp; MFA starter kit for filament.

3915.5k](/packages/rawilk-profile-filament-plugin)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

46273.9k](/packages/harris21-laravel-fuse)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
