← All documentation
Topics

- Working with data
- Designer components
- Field controls
- Search & filtering
- Calendars
- PDF & printing
- Email
- SMS
- Notifications
- Security
- Helper functions
- JavaScript API
- Performance

# 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.php` page & query parameters
- For developers — the `PPpdf` API
- 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.php` document 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).

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

- 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.

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:

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.

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:

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.

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

- Client-side. PDFs are rendered in the browser with the bundled `html2pdf.js`. No CDN, no server library — the file lives in `assets/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 `fromElement` with an explicit `margin` array 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()`).

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