PPPHP

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

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:

Click the small gear on the PDF card to set its defaults:

PDF is deliberately separate from Print (which opens the browser print dialog) and from Export (which downloads data as CSV / Excel / Word). PDF produces a real, downloadable document file.

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

Live options

Your choices are remembered per table in the browser, so the next PDF you open for that table starts with the same settings.

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:

ParameterMeaning
idRender 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.
orientationportrait or landscape — overrides the configured default.
embed1 renders only the document (no toolbar) — used internally by PPpdf.fromRecord().
download1 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.

MethodPurpose
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):

KeyDefaultDescription
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').
scale2Rendering resolution (higher = sharper & larger).
background'#ffffff'Canvas background colour.
outputIf set (e.g. 'blob', 'datauristring'), returns that instead of downloading.
Gotcha: to remove margins pass 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.

Two engines, one job. Client-side (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:

ParameterMeaning
tableThe table (must have the PDF page-type enabled).
idThe record id.
orientationportrait (default) or landscape.
marginCSS margin, e.g. 15mm or 20mm 15mm.
inline1 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.
dompdf supports a solid subset of HTML/CSS but not flexbox or grid — use table-based layouts and inline styles for server-rendered documents. (The built-in api/pdf.php already does this.)

How it works & limitations

Quick reference: PPpdf.fromRecord('<table>', <id>) is all you need to download a record’s PDF from anywhere in your app.