<h1>PDF Generation &amp; the <code>PPpdf</code> API</h1>
<p>
    Every generated application can turn its records into clean, professional <strong>PDF documents</strong>
    &mdash; entirely in the browser, with no server-side PDF library to install. This page explains it for
    two audiences: <strong>users</strong> (how to create and customise a PDF from a page) and
    <strong>developers</strong> (how to call the reusable <code>PPpdf</code> JavaScript API from anywhere in
    your app).
</p>
<p>
    Under the hood the app bundles <strong>html2pdf.js</strong> (jsPDF + html2canvas) locally at
    <code>assets/vendor/html2pdf/</code>. It is <em>lazy-loaded</em> — it downloads only the first time a PDF is
    actually requested, so pages that never make a PDF pay nothing.
</p>

<h2>Contents</h2>
<ul>
    <li><a href="#enable">Enabling PDF for a table</a></li>
    <li><a href="#users">For users — the PDF page &amp; live options</a></li>
    <li><a href="#page">The generated <code>pdf.php</code> page &amp; query parameters</a></li>
    <li><a href="#api">For developers — the <code>PPpdf</code> API</a></li>
    <li><a href="#fromelement">PPpdf.fromElement()</a></li>
    <li><a href="#fromrecord">PPpdf.fromRecord()</a></li>
    <li><a href="#open">PPpdf.open()</a></li>
    <li><a href="#ensurelib">PPpdf.ensureLib()</a></li>
    <li><a href="#examples">Real-world examples</a></li>
    <li><a href="#server">Server-side PDF (headless) — <code>PdfService</code> &amp; <code>api/pdf.php</code></a></li>
    <li><a href="#howitworks">How it works &amp; limitations</a></li>
</ul>

<!-- =========================================== -->
<h2 id="enable">Enabling PDF for a table</h2>
<p>
    PDF is a <strong>page-type</strong> in the <strong>Pages</strong> module. Open a project, go to
    <em>Pages</em>, select a table, and tick <strong>PDF</strong> under &ldquo;Pages to build&rdquo;. This does two
    things when you build the app:
</p>
<ul>
    <li>generates a dedicated <code>generated/&lt;table&gt;/pdf.php</code> document page, and</li>
    <li>adds a <strong>PDF</strong> button to that table&rsquo;s list toolbar and each record&rsquo;s View page.</li>
</ul>
<p>Click the small <i class="bi bi-sliders"></i> gear on the PDF card to set its defaults:</p>
<ul>
    <li><strong>Build the PDF from</strong> — <em>Print Page</em> (compact document layout) or <em>View Page</em>
        (record-card layout). These are mutually exclusive.</li>
    <li><strong>Orientation</strong> — Portrait or Landscape (the starting orientation; users can change it live).</li>
</ul>
<div class="alert alert-info">
    PDF is deliberately separate from <strong>Print</strong> (which opens the browser print dialog) and from
    <strong>Export</strong> (which downloads data as CSV / Excel / Word). PDF produces a real, downloadable
    document file.
</div>

<!-- =========================================== -->
<h2 id="users">For users — the PDF page &amp; live options</h2>
<p>
    Clicking a <strong>PDF</strong> button opens the document in a new tab and shows a live preview. The dark
    toolbar at the top lets you tailor the file <em>before</em> you download it — every change updates the
    preview instantly (&ldquo;what you see is what you get&rdquo;).
</p>
<h3>Toolbar</h3>
<ul>
    <li><strong>Download PDF</strong> — generates and saves the file (named after the record, e.g.
        <code>Invoice-42.pdf</code>).</li>
    <li><strong>Print</strong> — opens the browser print dialog for the same document.</li>
    <li><strong>Options</strong> — shows / hides the live options panel.</li>
</ul>
<h3>Live options</h3>
<ul>
    <li><strong>Orientation</strong> — switch between Portrait and Landscape; the page reshapes immediately.</li>
    <li><strong>Margins (mm)</strong> — set the <em>Top</em>, <em>Right</em>, <em>Bottom</em> and <em>Left</em>
        margins independently (0&ndash;60&nbsp;mm each). The whitespace you see is exactly what the PDF will have.</li>
    <li><strong>Margin mode</strong> — <em>Fit content</em> keeps everything on one image (best for a single
        record); <em>Every page</em> repeats the margins on every sheet (best for long, multi-record documents).</li>
    <li><strong>Custom header</strong> — optional text shown at the very top of the document (e.g.
        &ldquo;CONFIDENTIAL&rdquo;).</li>
    <li><strong>Custom footer</strong> — optional text shown at the very bottom (e.g. &ldquo;Generated by
        ACME&rdquo;).</li>
    <li><strong>Reset</strong> — returns everything to the table&rsquo;s configured defaults.</li>
</ul>
<div class="alert alert-success">
    Your choices are remembered per table in the browser, so the next PDF you open for that table starts with the
    same settings.
</div>

<!-- =========================================== -->
<h2 id="page">The generated <code>pdf.php</code> page &amp; query parameters</h2>
<p>
    Each PDF-enabled table gets <code>generated/&lt;table&gt;/pdf.php</code>. You can link to it directly. It
    accepts these query parameters:
</p>
<table class="table table-sm">
    <thead><tr><th>Parameter</th><th>Meaning</th></tr></thead>
    <tbody>
        <tr><td><code>id</code></td><td>Render a single record (e.g. <code>pdf.php?id=42</code>). Omit it to render the current filtered list.</td></tr>
        <tr><td><code>search</code>, <code>f[col]</code></td><td>Same list filters as the list page — the list-mode PDF respects them.</td></tr>
        <tr><td><code>orientation</code></td><td><code>portrait</code> or <code>landscape</code> — overrides the configured default.</td></tr>
        <tr><td><code>embed</code></td><td><code>1</code> renders only the document (no toolbar) — used internally by <code>PPpdf.fromRecord()</code>.</td></tr>
        <tr><td><code>download</code></td><td><code>1</code> auto-generates and downloads on load (used with <code>embed</code>).</td></tr>
    </tbody>
</table>
<h3>Examples</h3>
<pre><code>&lt;!-- Open the PDF page for order #42 --&gt;
&lt;a href="&lt;?= APP_BASE ?&gt;/generated/orders/pdf.php?id=42" target="_blank"&gt;Download PDF&lt;/a&gt;

&lt;!-- A landscape PDF of the current filtered list --&gt;
&lt;a href="pdf.php?&lt;?= http_build_query($_GET) ?&gt;&amp;orientation=landscape" target="_blank"&gt;List as PDF&lt;/a&gt;</code></pre>

<!-- =========================================== -->
<h2 id="api">For developers — the <code>PPpdf</code> API</h2>
<p>
    <code>window.PPpdf</code> is a small, reusable PDF helper that is included on <strong>every</strong> page of a
    generated app (via the shell). Call it from custom JavaScript, a field/row <strong>Click Action</strong>, a
    client-side <strong>Event</strong> handler, or any button you add. It lazy-loads the PDF engine on first use.
</p>
<table class="table table-sm">
    <thead><tr><th>Method</th><th>Purpose</th></tr></thead>
    <tbody>
        <tr><td><code>PPpdf.fromElement(el, options)</code></td><td>Make a PDF from any DOM element. Returns a Promise.</td></tr>
        <tr><td><code>PPpdf.fromRecord(table, id, options)</code></td><td>Make a record&rsquo;s PDF from its <code>pdf.php</code>, from anywhere. Returns a Promise.</td></tr>
        <tr><td><code>PPpdf.open(table, id)</code></td><td>Open a record&rsquo;s PDF page in a new tab.</td></tr>
        <tr><td><code>PPpdf.server(table, id, opts)</code></td><td>Download a <strong>server-rendered</strong> PDF (dompdf) &mdash; no browser rendering. See below.</td></tr>
        <tr><td><code>PPpdf.ensureLib()</code></td><td>Pre-load the PDF engine. Returns a Promise.</td></tr>
    </tbody>
</table>
<h3 id="options">Options object</h3>
<p>Passed to <code>fromElement</code> (and forwarded by <code>fromRecord</code>):</p>
<table class="table table-sm">
    <thead><tr><th>Key</th><th>Default</th><th>Description</th></tr></thead>
    <tbody>
        <tr><td><code>filename</code></td><td><code>'document'</code></td><td>Output name. <code>.pdf</code> is appended if missing.</td></tr>
        <tr><td><code>orientation</code></td><td><code>'portrait'</code></td><td><code>'portrait'</code> or <code>'landscape'</code>.</td></tr>
        <tr><td><code>margin</code></td><td><code>[10,10,12,10]</code></td><td>Page margins in mm — <code>[top, left, bottom, right]</code>. Use <code>[0,0,0,0]</code> for none.</td></tr>
        <tr><td><code>format</code></td><td><code>'a4'</code></td><td>Paper size passed to jsPDF (e.g. <code>'a4'</code>, <code>'letter'</code>).</td></tr>
        <tr><td><code>scale</code></td><td><code>2</code></td><td>Rendering resolution (higher = sharper &amp; larger).</td></tr>
        <tr><td><code>background</code></td><td><code>'#ffffff'</code></td><td>Canvas background colour.</td></tr>
        <tr><td><code>output</code></td><td>&mdash;</td><td>If set (e.g. <code>'blob'</code>, <code>'datauristring'</code>), returns that instead of downloading.</td></tr>
    </tbody>
</table>
<div class="alert alert-warning">
    <strong>Gotcha:</strong> to remove margins pass <code>margin: [0,0,0,0]</code>, <em>not</em> <code>margin: 0</code>.
    A plain <code>0</code> is falsy and falls back to the default margins.
</div>

<!-- =========================================== -->
<h2 id="fromelement">PPpdf.fromElement(el, options)</h2>
<p>Generates a PDF from a DOM element (or a CSS selector) and saves it. Returns a Promise that resolves when done.</p>
<pre><code>// 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);</code></pre>

<!-- =========================================== -->
<h2 id="fromrecord">PPpdf.fromRecord(table, id, options)</h2>
<p>
    The most convenient call: generate <strong>any record&rsquo;s</strong> PDF from anywhere, without navigating.
    It loads that record&rsquo;s <code>pdf.php</code> inside a hidden iframe (so the document keeps its own styling)
    and downloads it, honouring the table&rsquo;s configured orientation and filename.
</p>
<pre><code>// 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) =&gt; setTimeout(() =&gt; PPpdf.fromRecord('invoices', id), i * 1500));</code></pre>

<!-- =========================================== -->
<h2 id="open">PPpdf.open(table, id)</h2>
<p>Opens the full PDF page (with the live options studio) in a new tab, so the user can tweak and download.</p>
<pre><code>PPpdf.open('customers', 7);</code></pre>

<!-- =========================================== -->
<h2 id="ensurelib">PPpdf.ensureLib()</h2>
<p>
    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.
</p>
<pre><code>// Warm up the engine when the invoice view opens
document.addEventListener('DOMContentLoaded', () =&gt; { PPpdf.ensureLib(); });</code></pre>

<!-- =========================================== -->
<h2 id="examples">Real-world examples</h2>

<h3>1. A custom &ldquo;Download PDF&rdquo; button anywhere</h3>
<pre><code>&lt;button id="dlBtn"&gt;Download this order&lt;/button&gt;
&lt;script&gt;
document.getElementById('dlBtn').addEventListener('click', function () {
    this.disabled = true;
    PPpdf.fromRecord('orders', window.currentOrderId)
         .finally(() =&gt; { this.disabled = false; });
});
&lt;/script&gt;</code></pre>

<h3>2. From a row <em>Click Action</em> (Pages module)</h3>
<p>Set a field&rsquo;s click action to &ldquo;Execute Code&rdquo; and use the clicked row&rsquo;s id:</p>
<pre><code>// `row` is the clicked record; available in click-action code
PPpdf.fromRecord('invoices', row.id);</code></pre>

<h3>3. From an Event (e.g. after a record is saved)</h3>
<pre><code>// In an afterAddSave / afterEditSave client handler
PPpdf.fromRecord('receipts', payload.id, { orientation: 'portrait' });</code></pre>

<h3>4. PDF of a custom on-page element (not a whole record)</h3>
<pre><code>// A summary panel you built yourself
await PPpdf.fromElement('#monthly-summary', {
    filename: 'summary-' + new Date().toISOString().slice(0,7),
    orientation: 'landscape'
});</code></pre>

<h3>5. Generate a Blob and attach it (e.g. send to your own endpoint)</h3>
<pre><code>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' });</code></pre>

<h3>6. Open the studio for the user to customise</h3>
<pre><code>&lt;a href="#" onclick="PPpdf.open('customers', 7); return false;"&gt;Customise &amp; download PDF&lt;/a&gt;</code></pre>

<!-- =========================================== -->
<h2 id="server">Server-side PDF (headless) — <code>PdfService</code> &amp; <code>api/pdf.php</code></h2>
<p>
    Sometimes a PDF must be produced <strong>without a browser</strong> — to attach to an email, run on a
    schedule (cron), or process in a queue. For that, the app bundles the <strong>dompdf</strong> engine and a
    server-side <code>PdfService</code>. It renders HTML to a real PDF entirely in PHP.
</p>
<div class="alert alert-info">
    Two engines, one job. <strong>Client-side</strong> (<code>PPpdf</code> / html2pdf) is for interactive,
    in-page downloads. <strong>Server-side</strong> (<code>PdfService</code> / dompdf) is for headless generation
    the browser can&rsquo;t do. Pick whichever fits the situation.
</div>

<h3>The endpoint — <code>api/pdf.php</code></h3>
<p>PDF-enabled tables get a secure endpoint that streams a record&rsquo;s PDF, rendered on the server:</p>
<table class="table table-sm">
    <thead><tr><th>Parameter</th><th>Meaning</th></tr></thead>
    <tbody>
        <tr><td><code>table</code></td><td>The table (must have the PDF page-type enabled).</td></tr>
        <tr><td><code>id</code></td><td>The record id.</td></tr>
        <tr><td><code>orientation</code></td><td><code>portrait</code> (default) or <code>landscape</code>.</td></tr>
        <tr><td><code>margin</code></td><td>CSS margin, e.g. <code>15mm</code> or <code>20mm 15mm</code>.</td></tr>
        <tr><td><code>inline</code></td><td><code>1</code> to view in the browser instead of downloading.</td></tr>
    </tbody>
</table>
<p>It requires a signed-in user and only serves PDF-enabled tables. From the client, the easy way is:</p>
<pre><code>// 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
&lt;a href="&lt;?= APP_BASE ?&gt;/api/pdf.php?table=orders&amp;id=42"&gt;Download PDF&lt;/a&gt;</code></pre>

<h3>The service — <code>PdfService</code> (PHP)</h3>
<p>Use it from any server code — an event, a cron script, or your own endpoint:</p>
<pre><code>use PpPhp\Core\PdfService;

// Render any HTML to PDF bytes
$pdf = PdfService::fromHtml('&lt;h1&gt;Invoice #42&lt;/h1&gt;&lt;p&gt;Total: $128.00&lt;/p&gt;', [
    'orientation' =&gt; 'portrait',   // or 'landscape'
    'paper'       =&gt; 'A4',         // or 'letter', …
    'margin'      =&gt; '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()) { /* … */ }</code></pre>

<h3>Example — attach a PDF to an email</h3>
<pre><code>$path = PdfService::save($invoiceHtml, sys_get_temp_dir() . '/invoice-42.pdf');
// …then attach $path with your mail sending code.</code></pre>

<div class="alert alert-warning">
    dompdf supports a solid subset of HTML/CSS but <strong>not flexbox or grid</strong> — use table-based layouts
    and inline styles for server-rendered documents. (The built-in <code>api/pdf.php</code> already does this.)
</div>

<!-- =========================================== -->
<h2 id="howitworks">How it works &amp; limitations</h2>
<ul>
    <li><strong>Client-side.</strong> PDFs are rendered in the browser with the bundled <code>html2pdf.js</code>.
        No CDN, no server library — the file lives in <code>assets/vendor/html2pdf/</code> and is copied into
        every build.</li>
    <li><strong>Lazy-loaded.</strong> The engine (~900&nbsp;KB) downloads only on first PDF request.</li>
    <li><strong>WYSIWYG margins.</strong> On the studio page, margins are the document&rsquo;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.</li>
    <li><strong>Multi-page note.</strong> 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 <em>every</em> page, call
        <code>fromElement</code> with an explicit <code>margin</code> array instead.</li>
    <li><strong>Server-side PDF.</strong> Generating PDFs without a browser (for emailed or scheduled documents)
        is fully supported via the bundled dompdf engine &mdash; see
        <a href="#server">Server-side PDF</a> above (<code>PdfService</code>, <code>api/pdf.php</code>,
        <code>PPpdf.server()</code>).</li>
</ul>
<div class="alert alert-info">
    Quick reference: <code>PPpdf.fromRecord('&lt;table&gt;', &lt;id&gt;)</code> is all you need to download a
    record&rsquo;s PDF from anywhere in your app.
</div>
