Generating a PDF from HTML sounds like a solved problem.
In PHP, we already have tools such as dompdf. When browser rendering is required, Laravel applications can use Browsershot, Puppeteer, or a remote rendering service.
Those options work, but they represent two very different compromises:
- use a PHP-native renderer with a limited interpretation of the web platform;
- or ship an entire browser runtime to call
printToPDF.
I wanted to explore a third option.
That became Pliego: an open-source native HTML-to-PDF engine built on Servo for application-owned documents such as invoices, statements, purchase orders, and operational reports.
Pliego does not launch Chromium. It does not require Node.js or Java in the runtime. It also does not attempt to render arbitrary websites.
Its goal is narrower:
Provide a predictable document pipeline for trusted HTML and Blade views, with explicit rendering boundaries and useful evidence when something fails.
Repository: https://github.com/oxhq/pliego
The basic Laravel experience
The Laravel package can be installed through Composer:
composer require oxhq/pliego-laravel:^0.1.0
php artisan pliego:install
php artisan pliego:doctor
pliego:install downloads the runtime pinned by the installed package, verifies its size and SHA-256, and places it in the application-managed runtime directory.
pliego:doctor verifies the engine API, storage permissions, bundled font handling, and an offline PDF render.
Rendering a Blade view is intentionally small:
use Pliego\Laravel\Facades\Document;
return Document::view('invoice', compact('rows'))
->download('invoice.pdf');
Document-specific options can be added when needed:
return Document::view('invoices.show', [
'invoice' => $invoice,
])
->locale('es-MX')
->timezone('PST8PDT')
->denyNetwork()
->asset(
'fonts/invoice.woff2',
resource_path('fonts/invoice.woff2'),
)
->download('invoice.pdf');
The first release includes native runtime bundles for:
- Linux x86_64
- Windows x86_64
- macOS x86_64
- macOS arm64
Pliego is not a wrapper around printToPDF
Browsershot and similar tools generally follow this model:
Laravel
→ Node.js
→ Puppeteer
→ Chromium
→ printToPDF
Pliego follows a different pipeline:
Blade or HTML
→ Servo layout
→ canonical DocumentScene
→ preview, validation, evidence and PDF
Servo loads and lays out the document. Pliego then captures the computed document into a canonical scene containing pages, positioned glyphs, paths, images, links, fonts, and related resources.
The PDF backend consumes that scene directly.
It does not ask another engine to lay out or shape the document again.
This gives Pliego a useful internal boundary:
HTML/CSS/JS behavior
↓
DocumentScene
↓
PDF and diagnostic artifacts
The scene can be normalized and hashed. That makes it possible to inspect what Pliego captured, compare repeated renders, generate previews, and distinguish layout problems from PDF serialization problems.
What Pliego 0.1 supports
The current release focuses on common application-owned documents.
Its verified profile includes:
- authored page breaks;
- paged tables;
- repeated table headers;
- row keep-together constraints;
- selectable text;
- links;
- embedded TTF, OTF, WOFF, and WOFF2 fonts;
- local application assets;
- network-denied rendering by default;
- explicit allowlists for remote stylesheets, images, and fonts;
- retained input, scene, resource, PDF, and diagnostic artifacts;
- controlled JavaScript readiness;
- a deliberately bounded Chart.js 4.5.1 path.
The repository README includes two PDFs generated locally through a Laravel application using the released runtime:
- A two-page invoice with embedded fonts, an authored page break, allocation bars, a dense 20-row ledger, and calculated totals.
- A one-page operating report containing a Chart.js graph, summary metrics, and an account table.
The linked PDF remains text-selectable outside the rendered Chart.js canvas.
Static documents require no readiness API
A normal Blade view does not need custom JavaScript calls.
Pliego waits for the page load event and document.fonts.ready before capturing the result.
The explicit readiness API is only necessary when asynchronous work continues changing the document after load.
For example:
<script>
window.pliego?.defer();
loadReportData()
.then(drawReport)
.then(() => window.pliego?.ready())
.catch(error => window.pliego?.fail(error.message));
</script>
This avoids using arbitrary sleeps such as:
->setDelay(2000)
The document declares when it is finished instead of hoping that a fixed timeout is long enough.
Chart.js support without pretending to support everything
The initial Chart.js integration is intentionally specific.
The covered path uses:
- Chart.js 4.5.1;
- fixed canvas dimensions;
- animations and events disabled;
- the final chart draw completed synchronously;
- a full-canvas
getImageData()readback; - readiness signaled after that readback.
The retained RGBA pixels become the authoritative canvas result for the document scene.
That does not imply support for every Chart.js plugin, version, rendering mode, or Canvas API.
This distinction matters to Pliego.
A successful fixture is not treated as proof that an entire web platform feature is universally supported.
Fail closed instead of silently dropping content
One of the most important design decisions in Pliego is how unsupported rendering is handled.
Many document engines attempt to produce something even when parts of the document cannot be represented. That can be convenient, but it can also result in documents that look complete while silently missing visual information.
Pliego takes the opposite approach.
If the document uses paint outside the verified profile, the normal SDK path fails and does not publish a partial PDF.
The failure retains artifacts that can be inspected by the application or developer.
The current unsupported paint boundary includes features such as:
- CSS gradients;
- background-image layers;
- box and text shadows;
- text decorations;
- rounded and mixed-color borders;
- transforms;
- opacity;
- filters;
- blend modes;
- general clipping behavior.
This profile is currently narrow. That is deliberate.
Pliego is based on Servo, but it does not claim that every feature Servo can calculate is already represented safely and faithfully by the Pliego scene and PDF backend.
The public contract is what the complete pipeline can verify.
Resource handling is explicit
Pliego denies network access by default.
Assets can be supplied directly by the application:
Document::view('invoice')
->asset(
'fonts/inter.woff2',
resource_path('fonts/inter.woff2'),
)
->render();
Remote resources must be explicitly allowed:
Document::view('report')
->allowHttpRoot('https://fonts.googleapis.com/')
->allowHttpRoot('https://fonts.gstatic.com/s/')
->render();
Successful remote requests retain information such as:
- URL;
- response status;
- content type;
- byte count;
- SHA-256 digest.
Host-font fallback, unrestricted networking, redirects, and asset caching are disabled by default.
The objective is to reduce the number of invisible environmental inputs that can change a generated document.
What happens during a Laravel render
The Laravel integration:
- renders the Blade view to HTML;
- creates a private input bundle;
- copies only explicitly declared local assets;
- hashes those assets;
- records locale, timezone, page geometry, and resource policy;
- launches one native Pliego process;
- waits for the render result;
- validates that scene capture completed;
- returns the PDF or a typed exception.
A successful render() call returns paths to the PDF, input bundle, and retained artifacts.
$result = Document::view('invoice', compact('invoice'))
->render();
$pdfPath = $result->pdfPath;
$artifactsPath = $result->artifactsPath;
$inputBundlePath = $result->inputBundlePath;
A failed render preserves the diagnostic paths on the exception.
This is useful when a failure happens inside a queue worker or production environment where opening a browser inspector is not an option.
How does it compare to dompdf?
Pliego is not currently a drop-in replacement for dompdf.
Dompdf is mature, PHP-native, Composer-only, and much simpler to deploy. If it already renders a document correctly, there may be no reason to replace it.
Pliego uses a native Servo-based runtime and is aimed at documents that need some combination of:
- richer layout behavior;
- controlled JavaScript or Canvas output;
- explicit resource policies;
- retained rendering evidence;
- deterministic scene identities;
- fail-closed behavior.
The trade-off is a newer project, a native runtime dependency, and a deliberately narrower public rendering profile.
I have not yet published a fair performance comparison between the two, so Pliego currently makes no speed claim against dompdf.
How does it compare to Browsershot?
Browsershot gives an application access to Chromium, which means it starts with a much broader browser compatibility surface.
That is a major advantage when the input is essentially a web page.
Pliego is more constrained, but it also has a different operational model:
- no Chromium installation;
- no Node.js runtime;
- application-owned HTML rather than arbitrary pages;
- explicit networking;
- a canonical intermediate scene;
- retained evidence;
- failure when unsupported paint would make the document incomplete.
Performance also needs to be measured carefully.
A fair benchmark should distinguish between:
- cold Chromium startup;
- a persistent remote Chrome process;
- Pliego’s one-render-per-process runtime;
- PDF-only output;
- PDF plus Pliego’s previews and diagnostic evidence;
- memory usage and concurrency;
- short invoices and large statements.
Until that benchmark exists, the meaningful comparison is architectural rather than “X is faster than Y.”
Why Servo?
Servo provides a real web layout engine written primarily in Rust.
Pliego is maintained as a hard fork rather than as a thin wrapper that must continuously follow every upstream change.
The repository preserves Servo’s source layout so upstream fixes can be reviewed and selectively integrated. An upstream-main branch mirrors Servo, while temporary synchronization branches carry chosen changes into Pliego.
Not every Servo feature or refactor automatically belongs in a document renderer.
Updates can be evaluated based on their effect on:
- document layout;
- security;
- determinism;
- supported fixtures;
- maintenance cost;
- the public Pliego profile.
Servo provides the foundation. Pliego owns the document-oriented product boundary.
What Pliego is not
Pliego 0.1 is not:
- a general-purpose browser;
- a hosted PDF service;
- a renderer for hostile or tenant-authored HTML;
- a claim of complete CSS compatibility;
- a persistent rendering daemon;
- a universal replacement for dompdf, wkhtmltopdf, or Chromium.
It is an early open-source document engine with a specific goal:
Render trusted application documents through a native, inspectable, and predictable pipeline.
Try it
For Laravel:
composer require oxhq/pliego-laravel:^0.1.0
php artisan pliego:install
php artisan pliego:doctor
Then:
return Document::view('invoice', compact('rows'))
->download('invoice.pdf');
The repository contains the support profile, native release bundles, installation documentation, Laravel examples, generated PDFs, and focused rendering fixtures.
https://github.com/oxhq/pliego
The feedback that would be most useful at this stage is concrete:
- Did installation work in a clean project?
- Which real document failed?
- Was the failure report understandable?
- Which unsupported capability blocks adoption?
- Does the retained evidence help diagnose production rendering?
Pliego is still narrow.
Now it needs real documents to determine where it should grow.

Top comments (0)