PDF Generation & the PPpdf API
Every generated application can turn its records into clean, professional PDF documents
— entirely in the browser, with no server-side PDF library to install. This page explains it for
two audiences: users (how to create and customise a PDF from a page) and
developers (how to call the reusable PPpdf JavaScript API from anywhere in
your app).
Under the hood the app bundles html2pdf.js (jsPDF + html2canvas) locally at
assets/vendor/html2pdf/. It is lazy-loaded — it downloads only the first time a PDF is
actually requested, so pages that never make a PDF pay nothing.
Contents
- Enabling PDF for a table
- For users — the PDF page & live options
- The generated
pdf.phppage & query parameters - For developers — the
PPpdfAPI - PPpdf.fromElement()
- PPpdf.fromRecord()
- PPpdf.open()
- PPpdf.ensureLib()
- Real-world examples
- Server-side PDF (headless) —
PdfService&api/pdf.php - How it works & limitations
Enabling PDF for a table
PDF is a page-type in the Pages module. Open a project, go to Pages, select a table, and tick PDF under “Pages to build”. This does two things when you build the app:
- generates a dedicated
generated/<table>/pdf.phpdocument page, and - adds a PDF button to that table’s list toolbar and each record’s View page.
Click the small gear on the PDF card to set its defaults:
- Build the PDF from — Print Page (compact document layout) or View Page (record-card layout). These are mutually exclusive.
- Orientation — Portrait or Landscape (the starting orientation; users can change it live).
For users — the PDF page & live options
Clicking a PDF button opens the document in a new tab and shows a live preview. The dark toolbar at the top lets you tailor the file before you download it — every change updates the preview instantly (“what you see is what you get”).
Toolbar
- Download PDF — generates and saves the file (named after the record, e.g.
Invoice-42.pdf). - Print — opens the browser print dialog for the same document.
- Options — shows / hides the live options panel.
Live options
- Orientation — switch between Portrait and Landscape; the page reshapes immediately.
- Margins (mm) — set the Top, Right, Bottom and Left margins independently (0–60 mm each). The whitespace you see is exactly what the PDF will have.
- Margin mode — Fit content keeps everything on one image (best for a single record); Every page repeats the margins on every sheet (best for long, multi-record documents).
- Custom header — optional text shown at the very top of the document (e.g. “CONFIDENTIAL”).
- Custom footer — optional text shown at the very bottom (e.g. “Generated by ACME”).
- Reset — returns everything to the table’s configured defaults.
The generated pdf.php page & query parameters
Each PDF-enabled table gets generated/<table>/pdf.php. You can link to it directly. It
accepts these query parameters:
| Parameter | Meaning |
|---|---|
id | Render a single record (e.g. pdf.php?id=42). Omit it to render the current filtered list. |
search, f[col] | Same list filters as the list page — the list-mode PDF respects them. |
orientation | portrait or landscape — overrides the configured default. |
embed | 1 renders only the document (no toolbar) — used internally by PPpdf.fromRecord(). |
download | 1 auto-generates and downloads on load (used with embed). |
Examples
<!-- Open the PDF page for order #42 -->
<a href="<?= APP_BASE ?>/generated/orders/pdf.php?id=42" target="_blank">Download PDF</a>
<!-- A landscape PDF of the current filtered list -->
<a href="pdf.php?<?= http_build_query($_GET) ?>&orientation=landscape" target="_blank">List as PDF</a>
For developers — the PPpdf API
window.PPpdf is a small, reusable PDF helper that is included on every page of a
generated app (via the shell). Call it from custom JavaScript, a field/row Click Action, a
client-side Event handler, or any button you add. It lazy-loads the PDF engine on first use.
| Method | Purpose |
|---|---|
PPpdf.fromElement(el, options) | Make a PDF from any DOM element. Returns a Promise. |
PPpdf.fromRecord(table, id, options) | Make a record’s PDF from its pdf.php, from anywhere. Returns a Promise. |
PPpdf.open(table, id) | Open a record’s PDF page in a new tab. |
PPpdf.server(table, id, opts) | Download a server-rendered PDF (dompdf) — no browser rendering. See below. |
PPpdf.ensureLib() | Pre-load the PDF engine. Returns a Promise. |
Options object
Passed to fromElement (and forwarded by fromRecord):
| Key | Default | Description |
|---|---|---|
filename | 'document' | Output name. .pdf is appended if missing. |
orientation | 'portrait' | 'portrait' or 'landscape'. |
margin | [10,10,12,10] | Page margins in mm — [top, left, bottom, right]. Use [0,0,0,0] for none. |
format | 'a4' | Paper size passed to jsPDF (e.g. 'a4', 'letter'). |
scale | 2 | Rendering resolution (higher = sharper & larger). |
background | '#ffffff' | Canvas background colour. |
output | — | If set (e.g. 'blob', 'datauristring'), returns that instead of downloading. |
margin: [0,0,0,0], not margin: 0.
A plain 0 is falsy and falls back to the default margins.
PPpdf.fromElement(el, options)
Generates a PDF from a DOM element (or a CSS selector) and saves it. Returns a Promise that resolves when done.
// From an element reference
const card = document.getElementById('invoice');
await PPpdf.fromElement(card, { filename: 'invoice-42', orientation: 'portrait' });
// From a selector, landscape, no margins
await PPpdf.fromElement('#report', { orientation: 'landscape', margin: [0,0,0,0] });
// Get a Blob instead of downloading (e.g. to upload or preview)
const blob = await PPpdf.fromElement('#invoice', { output: 'blob' });
const url = URL.createObjectURL(blob);
window.open(url);
PPpdf.fromRecord(table, id, options)
The most convenient call: generate any record’s PDF from anywhere, without navigating.
It loads that record’s pdf.php inside a hidden iframe (so the document keeps its own styling)
and downloads it, honouring the table’s configured orientation and filename.
// Download order #42 as a PDF — from a dashboard, a menu, anywhere
PPpdf.fromRecord('orders', 42);
// Force landscape for this one
PPpdf.fromRecord('orders', 42, { orientation: 'landscape' });
// Batch: download a PDF for several records (staggered so downloads don't collide)
const ids = [12, 13, 14];
ids.forEach((id, i) => setTimeout(() => PPpdf.fromRecord('invoices', id), i * 1500));
PPpdf.open(table, id)
Opens the full PDF page (with the live options studio) in a new tab, so the user can tweak and download.
PPpdf.open('customers', 7);
PPpdf.ensureLib()
Pre-loads the PDF engine so the first real generation feels instant. Optional — every method calls it internally. Useful to warm it up on a page where you know a PDF is likely.
// Warm up the engine when the invoice view opens
document.addEventListener('DOMContentLoaded', () => { PPpdf.ensureLib(); });
Real-world examples
1. A custom “Download PDF” button anywhere
<button id="dlBtn">Download this order</button>
<script>
document.getElementById('dlBtn').addEventListener('click', function () {
this.disabled = true;
PPpdf.fromRecord('orders', window.currentOrderId)
.finally(() => { this.disabled = false; });
});
</script>
2. From a row Click Action (Pages module)
Set a field’s click action to “Execute Code” and use the clicked row’s id:
// `row` is the clicked record; available in click-action code
PPpdf.fromRecord('invoices', row.id);
3. From an Event (e.g. after a record is saved)
// In an afterAddSave / afterEditSave client handler
PPpdf.fromRecord('receipts', payload.id, { orientation: 'portrait' });
4. PDF of a custom on-page element (not a whole record)
// A summary panel you built yourself
await PPpdf.fromElement('#monthly-summary', {
filename: 'summary-' + new Date().toISOString().slice(0,7),
orientation: 'landscape'
});
5. Generate a Blob and attach it (e.g. send to your own endpoint)
const blob = await PPpdf.fromElement('#invoice', { output: 'blob', filename: 'invoice' });
const fd = new FormData();
fd.append('file', blob, 'invoice.pdf');
await fetch(PP_BASE + '/api/attach.php', { method: 'POST', body: fd, credentials: 'same-origin' });
6. Open the studio for the user to customise
<a href="#" onclick="PPpdf.open('customers', 7); return false;">Customise & download PDF</a>
Server-side PDF (headless) — PdfService & api/pdf.php
Sometimes a PDF must be produced without a browser — to attach to an email, run on a
schedule (cron), or process in a queue. For that, the app bundles the dompdf engine and a
server-side PdfService. It renders HTML to a real PDF entirely in PHP.
PPpdf / html2pdf) is for interactive,
in-page downloads. Server-side (PdfService / dompdf) is for headless generation
the browser can’t do. Pick whichever fits the situation.
The endpoint — api/pdf.php
PDF-enabled tables get a secure endpoint that streams a record’s PDF, rendered on the server:
| Parameter | Meaning |
|---|---|
table | The table (must have the PDF page-type enabled). |
id | The record id. |
orientation | portrait (default) or landscape. |
margin | CSS margin, e.g. 15mm or 20mm 15mm. |
inline | 1 to view in the browser instead of downloading. |
It requires a signed-in user and only serves PDF-enabled tables. From the client, the easy way is:
// Download a server-rendered PDF (no browser rendering)
PPpdf.server('orders', 42);
PPpdf.server('orders', 42, { orientation: 'landscape', margin: '20mm' });
PPpdf.server('orders', 42, { inline: true }); // open in a new tab
// …or link straight to it
<a href="<?= APP_BASE ?>/api/pdf.php?table=orders&id=42">Download PDF</a>
The service — PdfService (PHP)
Use it from any server code — an event, a cron script, or your own endpoint:
use PpPhp\Core\PdfService;
// Render any HTML to PDF bytes
$pdf = PdfService::fromHtml('<h1>Invoice #42</h1><p>Total: $128.00</p>', [
'orientation' => 'portrait', // or 'landscape'
'paper' => 'A4', // or 'letter', …
'margin' => '15mm',
]);
// Save it to a file (e.g. to attach to an email)
PdfService::save($html, __DIR__ . '/../storage/invoices/42.pdf');
// Stream it to the browser as a download
PdfService::stream($html, 'invoice-42');
// Feature-detect before using
if (PdfService::available()) { /* … */ }
Example — attach a PDF to an email
$path = PdfService::save($invoiceHtml, sys_get_temp_dir() . '/invoice-42.pdf');
// …then attach $path with your mail sending code.
api/pdf.php already does this.)
How it works & limitations
- Client-side. PDFs are rendered in the browser with the bundled
html2pdf.js. No CDN, no server library — the file lives inassets/vendor/html2pdf/and is copied into every build. - Lazy-loaded. The engine (~900 KB) downloads only on first PDF request.
- WYSIWYG margins. On the studio page, margins are the document’s own padding and the PDF is produced with zero page margin, so the preview matches the output exactly. This is ideal for single-record documents.
- Multi-page note. For very long, multi-page documents the padding-based margins appear once
(top of the first page, bottom of the last). If you need identical margins on every page, call
fromElementwith an explicitmarginarray instead. - Server-side PDF. Generating PDFs without a browser (for emailed or scheduled documents)
is fully supported via the bundled dompdf engine — see
Server-side PDF above (
PdfService,api/pdf.php,PPpdf.server()).
PPpdf.fromRecord('<table>', <id>) is all you need to download a
record’s PDF from anywhere in your app.