Skip to main content
    Courses/PHP/PDF Generation

    Lesson 33 • Advanced

    PDF Generation & File Exporting 📄

    By the end of this lesson you'll turn HTML and CSS into a polished invoice PDF, stamp every page with a header, footer and page number, drop in a logo, and choose between streaming a download or saving the file — the exact skills behind every "Download as PDF" button you've ever clicked.

    What You'll Learn in This Lesson

    • Pick the right PDF library — Dompdf, mPDF, or TCPDF/FPDF — for the job
    • Generate a styled invoice from an HTML + CSS template with Dompdf
    • Add a header, footer, page numbers and a logo image with TCPDF
    • Embed images and Unicode characters without getting empty boxes
    • Choose between streaming a download and saving the PDF to disk
    • Avoid the memory, font and "output before stream" gotchas that break PDFs

    1️⃣ Choosing a PDF Library

    PHP has no built-in PDF maker, so you add one with Composer (PHP's package manager). The three families you'll meet: Dompdf and mPDF turn an HTML + CSS template into a PDF — ideal when you can describe the document as a web page. TCPDF and the classic FPDF are programmatic: you place each piece of text, box and image at exact coordinates. mPDF is the pick when you need strong Unicode or right-to-left scripts like Arabic. Start from how you want to describe the page, and the choice falls out.

    Picking the right tool
    <?php
    // PHP can't make a PDF on its own — you pull in a library with Composer.
    // The right one depends on HOW you want to describe the document.
    
    // 1) Dompdf  — you already have an HTML + CSS template? Convert it.
    //    composer require dompdf/dompdf
    //
    // 2) mPDF    — like Dompdf but better Unicode + RTL (Arabic/Hebrew) support.
    //    composer require mpdf/mpdf
    //
    // 3) TCPDF / FPDF — you place every line, box and image by coordinate.
    //    composer require tecnickcom/tcpdf      // TCPDF: feature-rich
    //    composer require setasign/fpdf         // FPDF:  small + classic
    
    // Rule of thumb, printed so you remember it:
    echo "Have an HTML template?      -> Dompdf or mPDF\n";
    echo "Need Arabic / emoji / RTL?  -> mPDF\n";
    echo "Need pixel-exact layout?    -> TCPDF or FPDF\n";
    Output
    Have an HTML template?      -> Dompdf or mPDF
    Need Arabic / emoji / RTL?  -> mPDF
    Need pixel-exact layout?    -> TCPDF or FPDF
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    2️⃣ HTML → PDF: an Invoice with Dompdf

    This is the most common real-world path. You build the invoice as a normal HTML string with a <style> block, then Dompdf renders it to PDF bytes. Two things to notice: the CSS lives inside the HTML (Dompdf won't fetch a separate .css file), and output() hands you the raw bytes so you decide whether to save, stream, or email them.

    Invoice from an HTML + CSS template
    <?php
    // Dompdf renders an HTML + CSS string into a PDF.
    // composer require dompdf/dompdf
    require_once "vendor/autoload.php";
    
    use Dompdf\\Dompdf;
    use Dompdf\\Options;
    
    // Build the invoice as a normal HTML string. CSS lives in a <style> block
    // because Dompdf does NOT fetch external .css files by default.
    function invoiceHtml(array $d): string
    {
        return <<<HTML
        <!DOCTYPE html>
        <html><head><style>
            body  { font-family: DejaVu Sans; color: #333; }
            .logo { font-size: 26px; font-weight: bold; color: #04AA6D; }
            table { width: 100%; border-collapse: collapse; margin-top: 16px; }
            th    { background: #04AA6D; color: #fff; padding: 8px; text-align: left; }
            td    { padding: 8px; border-bottom: 1px solid #ddd; }
        </style></head>
        <body>
            <div class="logo">{$d['company']}</div>
            <h1>Invoice #{$d['num']}</h1>
            <p>Date: {$d['date']}</p>
            <table>
              <tr><th>Item</th><th>Total</th></tr>
              <tr><td>Web Development</td><td>\$3,000</td></tr>
              <tr><td>Hosting (annual)</td><td>\$120</td></tr>
            </table>
        </body></html>
        HTML;
    }
    
    $options = new Options();
    $options->set("defaultFont", "DejaVu Sans"); // a font with full Unicode coverage
    
    $dompdf = new Dompdf($options);
    $dompdf->loadHtml(invoiceHtml([
        "company" => "ACME CORP",
        "num"     => "1001",
        "date"    => "2025-03-15",
    ]));
    $dompdf->setPaper("A4", "portrait"); // page size + orientation
    $dompdf->render();                   // turn the HTML into PDF bytes
    
    // Save the PDF to disk (the bytes are returned by output()):
    file_put_contents(__DIR__ . "/invoice-1001.pdf", $dompdf->output());
    echo "Saved invoice-1001.pdf\n";
    Output
    Saved invoice-1001.pdf
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    The PDF will look almost exactly like the HTML would in a browser — same green header, same table — which is why Dompdf is so productive. The catch is that it understands only a subset of CSS (no flexbox, no grid); you'll see how to dodge that in Common Errors.

    3️⃣ Headers, Footers, Page Numbers & Images

    Multi-page documents need the same logo and page numbers on every page. With TCPDF you get this almost for free: override the Header() and Footer() methods and TCPDF calls them automatically on each new page. Image() drops a logo at exact coordinates, and getAliasNumPage() / getAliasNbPages() fill in "Page X of Y" — TCPDF substitutes the real totals after the whole document is built.

    Repeating header, footer + page numbers (TCPDF)
    <?php
    // TCPDF places everything by coordinate — perfect for headers, footers,
    // page numbers and logos that must appear on EVERY page.
    // composer require tecnickcom/tcpdf
    require_once "vendor/autoload.php";
    
    class InvoicePdf extends TCPDF
    {
        // Header() runs automatically at the top of every page.
        public function Header(): void
        {
            // Image(file, x, y, width)  — a logo in the top-left corner.
            $this->Image(__DIR__ . "/logo.png", 15, 10, 30);
            $this->SetFont("helvetica", "B", 14);
            $this->Cell(0, 10, "ACME CORP — INVOICE", 0, 1, "R"); // R = right-aligned
        }
    
        // Footer() runs automatically at the bottom of every page.
        public function Footer(): void
        {
            $this->SetY(-15);                       // 15mm up from the bottom
            $this->SetFont("helvetica", "", 9);
            // getAliasNumPage()/getAliasNbPages() = current page / total pages.
            $this->Cell(0, 10, "Page " . $this->getAliasNumPage()
                . " of " . $this->getAliasNbPages(), 0, 0, "C"); // C = centred
        }
    }
    
    $pdf = new InvoicePdf("P", "mm", "A4"); // Portrait, millimetres, A4
    $pdf->SetMargins(15, 30, 15);           // left, top, right
    $pdf->AddPage();
    
    $pdf->SetFont("helvetica", "", 12);
    $pdf->Cell(0, 8, "Bill To: Alice Smith", 0, 1); // 0 width = full line, 1 = new line
    
    $pdf->AddPage(); // forces a 2nd page so "Page 1 of 2" appears in the footer
    $pdf->Cell(0, 8, "Terms: payment due within 30 days.", 0, 1);
    
    $pdf->Output(__DIR__ . "/invoice.pdf", "F"); // "F" = save to a File
    echo "Saved invoice.pdf (2 pages, numbered footer)\n";
    Output
    Saved invoice.pdf (2 pages, numbered footer)
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    4️⃣ Streaming vs Saving

    Once the PDF exists you must say where it goes. Stream it and the bytes flow to the browser — either viewed inline in the tab or popped as a download dialog. Save it and the bytes land in a file you can store, attach to an email, or serve later. The destination is just a flag — Dompdf's stream() vs output(), or TCPDF's Output(name, "I"|"D"|"F"|"S") — but there's one golden rule beside it.

    Choosing the destination
    <?php
    // Where does the PDF GO? Dompdf and TCPDF both let you choose. Picking the
    // wrong destination is the #1 reason a PDF "downloads as corrupt".
    
    // --- Dompdf ---
    // stream() sends the PDF to the browser. Attachment=false shows it inline;
    // Attachment=true pops a download dialog.
    $dompdf->stream("invoice.pdf", ["Attachment" => true]);  // download
    $dompdf->stream("invoice.pdf", ["Attachment" => false]); // view in the tab
    
    // $dompdf->output() returns the raw bytes instead, so YOU decide:
    $bytes = $dompdf->output();
    file_put_contents("invoice.pdf", $bytes);   // save to disk
    // mail()->attach($bytes);                   // ...or email it
    
    // --- TCPDF Output(name, dest) ---
    // "I" = Inline (view)   "D" = Download   "F" = save to File   "S" = return String
    $pdf->Output("invoice.pdf", "D"); // force a download
    $saved = $pdf->Output("", "S");   // get the bytes back to store or queue
    
    // GOLDEN RULE: when you stream, the PDF bytes must be the FIRST thing sent.
    // Nothing — no echo, no stray space before <?php — may go out before it.
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    The golden rule: when you stream, the PDF bytes must be the first thing the server sends. A stray echo, a blank line, or even a space before <?php sneaks ahead of the PDF and corrupts it — the browser downloads a file that won't open. If you don't need to stream, use output() / Output("", "S") to grab the bytes safely.

    5️⃣ Your Turn: Finish the Dompdf Script

    Now you try. The script below is wired up except for the three method calls that actually do the work. Fill in each ___ using the 👉 hint, then run it in a project where you've installed Dompdf and check it against the Output panel.

    🎯 Your turn: load → render → save
    <?php
    // 🎯 YOUR TURN — finish this Dompdf script, then run it in your project.
    // Everything is wired up except the three lines that DO the work.
    require_once "vendor/autoload.php";
    use Dompdf\\Dompdf;
    
    $html = "<h1>Hello PDF</h1><p>My first generated document.</p>";
    
    $dompdf = new Dompdf();
    
    // 1) Load the HTML string into Dompdf
    $dompdf->___($html);          // 👉 the method that loads HTML is loadHtml
    
    // 2) Render it (HTML -> PDF bytes)
    $dompdf->___();               // 👉 the method is render
    
    // 3) Save the bytes to a file
    file_put_contents("hello.pdf", $dompdf->___()); // 👉 output() returns the bytes
    
    echo "Done\n";
    
    // ✅ Expected output:
    //    Done
    //    (and a hello.pdf file appears next to your script)
    Output
    Done
    Fill in the three ___ blanks with loadHtml, render and output. It should print Done and write a hello.pdf.

    One more — this time a TCPDF footer that needs to show "Page X of Y". Fill in the two helpers that return the current page and the total.

    🎯 Your turn: a numbered footer
    <?php
    // 🎯 YOUR TURN — make the footer show "Page X of Y" on every page.
    // Fill in the two TCPDF helpers that return the page numbers.
    require_once "vendor/autoload.php";
    
    class Report extends TCPDF
    {
        public function Footer(): void
        {
            $this->SetY(-15);
            $this->SetFont("helvetica", "", 9);
    
            $current = $this->___();   // 👉 current page number  -> getAliasNumPage
            $total   = $this->___();   // 👉 total page count      -> getAliasNbPages
    
            $this->Cell(0, 10, "Page {$current} of {$total}", 0, 0, "C");
        }
    }
    
    $pdf = new Report("P", "mm", "A4");
    $pdf->AddPage();
    $pdf->AddPage(); // two pages so totals are visible
    $pdf->Output("report.pdf", "F");
    echo "report.pdf written\n";
    
    // ✅ Expected output:
    //    report.pdf written
    //    (every page footer reads "Page 1 of 2", "Page 2 of 2")
    Output
    report.pdf written
    Replace the two ___ with getAliasNumPage() and getAliasNbPages(). Every footer should read "Page 1 of 2", "Page 2 of 2".

    Common Errors (and the fix)

    • Your layout collapses — flexbox / grid is ignored — Dompdf is not a browser and supports only a subset of CSS. display: flex and grid simply do nothing. Lay PDFs out with tables and fixed widths, and put CSS in a <style> block or inline — external .css files aren't fetched by default.
    • "Allowed memory size of … bytes exhausted" — a big table or a high-resolution image blew past PHP's memory_limit while rendering. Raise it for the script with ini_set('memory_limit', '256M'), shrink images before embedding them, and split huge reports across pages instead of one giant page.
    • £, €, accents or emoji show as empty boxes — the active font has no glyph for those characters. Use a font with wide coverage (DejaVu Sans ships with both Dompdf and TCPDF) and make sure your HTML is UTF-8. For Arabic or Hebrew, switch to mPDF.
    • The downloaded PDF won't open / "file is damaged" — something was printed before the PDF bytes. When streaming, the PDF must be the first output, so remove any stray echo, blank line, or space before <?php (and drop the closing ?> tag). Or save with output() instead of streaming.

    Pro Tips

    • 💡 Generate slow PDFs in the background. A 10-page report can take several seconds — queue it as a background job and email the user a download link instead of blocking the request.
    • 💡 Design the template in a browser first. Build and tweak the HTML in Chrome, then hand it to Dompdf — but keep to tables and simple CSS so it survives the conversion.
    • 💡 Set a Unicode default font once. DejaVu Sans covers Latin, currency symbols and accents, so you avoid the empty-box surprise on a customer's name.

    📋 Quick Reference — PHP PDF Generation

    TaskCodeNotes
    Install Dompdfcomposer require dompdf/dompdfHTML + CSS → PDF
    Install TCPDFcomposer require tecnickcom/tcpdfCoordinate-based
    Render HTML$d->loadHtml($h); $d->render();Dompdf
    Save bytesfile_put_contents($f, $d->output());to a file
    Download$d->stream("x.pdf");browser download
    TCPDF page$p->AddPage(); $p->Cell(...);add page + text
    Page numbersgetAliasNumPage() / getAliasNbPages()in Footer()
    TCPDF dest$p->Output($n, "I"|"D"|"F"|"S");view/down/file/string

    Frequently Asked Questions

    Q: Which PHP PDF library should I use?

    If you already have an HTML and CSS template, use Dompdf — it converts HTML straight to PDF and is the quickest path for most invoices and reports. Use mPDF when you need solid Unicode, emoji, or right-to-left languages like Arabic and Hebrew, since its text handling is stronger than Dompdf's. Reach for TCPDF or the older FPDF when you need pixel-exact placement — barcodes, shipping labels, forms — where you position every element by coordinate. All three install with Composer and are free.

    Q: Why does my CSS look wrong in the generated PDF?

    Dompdf is not a browser — it supports a useful but limited subset of CSS. Flexbox and CSS grid are not supported, external stylesheets are not fetched by default, and many modern properties are ignored. Build PDF templates with old-school layout: tables for columns, inline styles or a <style> block instead of a linked file, and fixed widths in pt or mm. mPDF supports a bit more, but the same advice applies — keep the CSS simple and test early.

    Q: My PDF downloads but won't open — what went wrong?

    Almost always there was output before the PDF bytes. When you stream a PDF, those bytes must be the very first thing the server sends, so a stray echo, a blank line, or a space before the opening <?php tag corrupts the file. Remove any output before the stream() or Output() call, and don't leave a trailing ?> with whitespace after it. If you only need the bytes to save or email, use output() (Dompdf) or Output("", "S") (TCPDF) instead of streaming.

    Q: Why is my PHP script running out of memory generating PDFs?

    PDF rendering holds the whole document in memory, and large tables or high-resolution images push past PHP's default memory_limit. Raise it for the script with ini_set('memory_limit', '256M'), resize images before embedding them, and paginate huge reports instead of building one giant page. For documents that take several seconds, generate them in a background queue job and email the user a link rather than making them wait on the request.

    Q: Why do special characters like £, €, or accents show as boxes?

    Those boxes mean the active font has no glyph for the character. PDF fonts only draw the characters they actually contain, so a basic Latin font can't render £, €, é, or emoji. Switch to a font with wide coverage — DejaVu Sans ships with both Dompdf and TCPDF — and make sure your HTML source is UTF-8 encoded. For Arabic, Hebrew, or complex scripts, mPDF handles shaping and direction better than the others.

    Mini-Challenge: A Receipt PDF

    No code is filled in this time — just a brief and an outline. Write it yourself in a project with Dompdf installed, run it, then check your result against the expected output in the comments. This is exactly the build-render-save loop you'll use on every real document.

    🎯 Mini-Challenge: build a one-page receipt
    <?php
    // 🎯 MINI-CHALLENGE: a one-page receipt PDF with Dompdf.
    // No code is filled in — work from the steps, then run it in your project.
    //
    // 1. require "vendor/autoload.php" and  use Dompdf\Dompdf;
    // 2. Build an HTML string with:
    //      - an <h1> "Receipt #500"
    //      - a <table> of 2 items and their prices (put CSS in a <style> block)
    // 3. $dompdf = new Dompdf();
    // 4. loadHtml() your HTML, then setPaper("A4", "portrait"), then render()
    // 5. Save it:  file_put_contents("receipt.pdf", $dompdf->output());
    // 6. echo "receipt.pdf saved\n";
    //
    // Remember: keep CSS inline / in a <style> block, and use a Unicode font
    // (DejaVu Sans) if you include a £ or € sign.
    //
    // ✅ Expected output:
    //    receipt.pdf saved
    //    (a receipt.pdf file appears next to your script)
    
    // your code here
    Build an HTML receipt string, load it into Dompdf, render and save it with output(). It should print receipt.pdf saved and write the file.

    🎉 Lesson Complete!

    • ✅ PHP makes PDFs through Composer libraries — there's no built-in maker
    • Dompdf / mPDF convert HTML + CSS; TCPDF / FPDF place elements by coordinate
    • ✅ Build invoices from an HTML template, keeping CSS inline or in a <style> block
    • ✅ Override Header() / Footer() for logos and "Page X of Y" on every page
    • Stream bytes to the browser or save them — but stream nothing before the PDF
    • ✅ Watch the gotchas: limited CSS, memory on big PDFs, and Unicode fonts
    • Next lesson: Localization — build multilingual PHP apps with translations and locale-aware formatting

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service