StackPractices
intermediate By Mathias Paulenko

Generate PDFs

How to generate PDF documents programmatically from HTML, templates, or raw data.

Overview

PDF generation is a common requirement for invoices, reports, certificates, and legal documents. Modern libraries let you create PDFs from HTML templates, which means your design team can style documents with CSS while your backend fills in live data. The solution below covers the most reliable approaches in Python, JavaScript, and Java.

When to Use

Use this resource when:

  • You need to generate invoices, receipts, or order confirmations. See Export CSV Excel for tabular data exports.
  • Users request downloadable reports or analytics exports. See Background Jobs for async PDF generation.
  • You must produce legally compliant documents (contracts, certificates). See Email Templates MJML for professional email delivery.
  • You want to reuse existing HTML/CSS designs for print output. See Image Optimization for embedded image optimization.

Solution

Python (WeasyPrint)

from weasyprint import HTML, CSS
from jinja2 import Template

html_template = """
<!DOCTYPE html>
<html>
<head><style>
  body { font-family: Arial; margin: 40px; }
  h1 { color: #333; }
  .total { font-weight: bold; font-size: 1.2em; }
</style></head>
<body>
  <h1>Invoice #{{ invoice_id }}</h1>
  <p>Customer: {{ customer }}</p>
  <p class="total">Total: ${{ total }}</p>
</body>
</html>
"""

def generate_invoice(invoice_id, customer, total):
    template = Template(html_template)
    html_out = template.render(invoice_id=invoice_id, customer=customer, total=total)
    HTML(string=html_out).write_pdf(f"invoice_{invoice_id}.pdf")

generate_invoice("12345", "Acme Corp", "1,250.00")

JavaScript (Puppeteer)

const puppeteer = require("puppeteer");
const handlebars = require("handlebars");

const template = handlebars.compile(`
  <html>
  <head><style>
    body { font-family: Arial; margin: 40px; }
    h1 { color: #333; }
    .total { font-weight: bold; font-size: 1.2em; }
  </style></head>
  <body>
    <h1>Invoice #{{invoiceId}}</h1>
    <p>Customer: {{customer}}</p>
    <p class="total">Total: ${{total}}</p>
  </body>
  </html>
`);

async function generatePDF(data, outputPath) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  const html = template(data);
  await page.setContent(html, { waitUntil: "networkidle0" });
  await page.pdf({ path: outputPath, format: "A4", printBackground: true });
  await browser.close();
}

generatePDF(
  { invoiceId: "12345", customer: "Acme Corp", total: "1,250.00" },
  "invoice_12345.pdf"
);

Java (OpenPDF + Thymeleaf)

import com.lowagie.text.Document;
import com.lowagie.text.Paragraph;
import com.lowagie.text.pdf.PdfWriter;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;

import java.io.FileOutputStream;

public class PdfGenerator {
    public static void generate(String outputPath, String customer, String total) throws Exception {
        // Thymeleaf HTML template
        TemplateEngine engine = new TemplateEngine();
        Context ctx = new Context();
        ctx.setVariable("customer", customer);
        ctx.setVariable("total", total);
        String html = engine.process("invoice-template", ctx);

        // Convert HTML to PDF with OpenPDF + flying-saucer
        Document document = new Document();
        PdfWriter.getInstance(document, new FileOutputStream(outputPath));
        document.open();
        document.add(new Paragraph("Invoice for " + customer));
        document.add(new Paragraph("Total: " + total));
        document.close();
    }
}

Explanation

There are two primary approaches to PDF generation:

  1. HTML-to-PDF: Render HTML+CSS into PDF (WeasyPrint, Puppeteer, wkhtmltopdf). Best for complex layouts and reuse of web designs.
  2. Native API: Build PDFs programmatically with low-level libraries (iText, OpenPDF, PDFBox). Best for fine-grained control and small file sizes.

HTML-to-PDF is the dominant approach today because it separates presentation (CSS) from data (template variables), enabling non-developers to tweak designs.

Variants

ApproachLibraryProsCons
HTML-to-PDFWeasyPrintPure Python, good CSSNo JS, limited fonts
HTML-to-PDFPuppeteerFull Chrome engineHeavy (~100 MB), slower
HTML-to-PDFPlaywrightModern, maintainedSimilar weight to Puppeteer
Native APIiText / OpenPDFFast, small filesVerbose code, no CSS
Native APIPDFBoxApache license, matureComplex for simple docs

What Works

  • Use HTML templates for complex layouts: Designers can edit CSS without touching code.
  • Embed fonts: System fonts vary across OSs. Embed a web font for consistency.
  • Set page margins and headers/footers: Use @page CSS rules for print-friendly layouts.
  • Generate asynchronously: PDF creation is CPU-intensive. Use a queue for large batches.
  • Validate input before rendering: Sanitize HTML to prevent injection attacks in templates.

Common Mistakes

  • Using headless Chrome for every single PDF: Startup overhead is ~1s. Reuse browser instances or use a pool.
  • Not embedding images as base64: External image URLs fail when the PDF is viewed offline.
  • Ignoring page breaks: Long tables overflow awkwardly without page-break-inside: avoid.
  • Hardcoding paths: Use temp directories or streams, not /tmp/output.pdf.
  • Forgetting to close the browser / document: Leaks memory and file handles.

Additional Best Practices

  1. Stream PDF output instead of writing to disk. Return PDFs as byte streams to avoid disk I/O and temp file cleanup. This is essential for serverless deployments where disk space is ephemeral:
from weasyprint import HTML
import io

def generate_pdf_stream(html_content: str) -> bytes:
    """Generate PDF as bytes without writing to disk."""
    buffer = io.BytesIO()
    HTML(string=html_content).write_pdf(buffer)
    return buffer.getvalue()

# Flask: return generate_pdf_stream(html), mimetype='application/pdf'
  1. Subset fonts to reduce file size. Full font files can add 200KB-2MB per family. WeasyPrint subsets automatically. For Puppeteer, use --font-render-hinting=none and embed only the weights you need:
// Only embed the font weights you actually use
const html = `
<style>
  @font-face {
    font-family: 'Inter';
    src: url('data:font/woff2;base64,${interRegularBase64}') format('woff2');
    font-weight: 400;
    font-style: normal;
  }
  body { font-family: 'Inter', sans-serif; }
</style>
<h1>Hello World</h1>
`;
  1. Add PDF metadata for searchability. Set title, author, subject, and keywords in the PDF properties. This improves search engine indexing and desktop search:
from weasyprint import HTML

def generate_pdf_with_metadata(html: str, output_path: str, metadata: dict) -> None:
    doc = HTML(string=html).render()
    doc.pages[0].document.info.update({
        'Title': metadata.get('title', ''),
        'Author': metadata.get('author', ''),
        'Subject': metadata.get('subject', ''),
        'Keywords': metadata.get('keywords', ''),
    })
    doc.write_pdf(output_path)

# generate_pdf_with_metadata(html, "invoice.pdf", {
#     "title": "Invoice #12345",
#     "author": "ACME Inc.",
#     "subject": "Payment due in 30 days",
#     "keywords": "invoice, acme, 12345"
# })

Additional Common Mistakes

  1. Not handling PDF generation timeouts. Complex HTML with external resources can hang indefinitely. Set timeouts on Puppeteer and WeasyPrint:
const puppeteer = require('puppeteer');

async function generatePdfWithTimeout(html, timeoutMs = 30000) {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    try {
        await page.setContent(html, {
            waitUntil: 'networkidle0',
            timeout: timeoutMs,
        });
        const pdf = await page.pdf({
            format: 'A4',
            printBackground: true,
            timeout: timeoutMs,
        });
        return pdf;
    } catch (err) {
        if (err.name === 'TimeoutError') {
            throw new Error(`PDF generation timed out after ${timeoutMs}ms`);
        }
        throw err;
    } finally {
        await browser.close();
    }
}
  1. Generating PDFs on the main thread in Node.js. PDF generation is CPU-intensive and blocks the event loop. Use worker threads or a separate process:
const { Worker } = require('worker_threads');
const path = require('path');

function generatePdfInWorker(html, options) {
    return new Promise((resolve, reject) => {
        const worker = new Worker(path.join(__dirname, 'pdf-worker.js'), {
            workerData: { html, options },
        });
        worker.on('message', resolve);
        worker.on('error', reject);
        worker.on('exit', (code) => {
            if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
        });
    });
}

// pdf-worker.js:
// const { parentPort, workerData } = require('worker_threads');
// const puppeteer = require('puppeteer');
// (async () => {
//     const browser = await puppeteer.launch();
//     const page = await browser.newPage();
//     await page.setContent(workerData.html, { waitUntil: 'networkidle0' });
//     const pdf = await page.pdf(workerData.options);
//     await browser.close();
//     parentPort.postMessage(pdf);
// })();
  1. Not testing PDF output across PDF readers. PDFs render differently in Adobe Reader, Chrome, Firefox, and Preview. Test with at least Chrome and Adobe Reader. Common issues: font fallback differences, CSS @page support, and form field rendering.

Frequently Asked Questions

Can I generate a PDF from a React/Vue component?

Yes, with Puppeteer or Playwright. Render the component to HTML on the server (SSR), then pass the HTML string to the PDF engine. Some frameworks (Next.js) offer built-in PDF export APIs.

How do I add digital signatures to PDFs?

Use iText (Java) or PyPDF2 + a crypto library (Python). You need an X.509 certificate and private key. For production, use a hardware security module (HSM) or cloud signing service (AWS CloudHSM, Azure Key Vault).

Why is my PDF much larger than expected?

Embedded fonts and uncompressed images are the usual culprits. Subset fonts (only include used glyphs) and compress images before embedding. WeasyPrint and Puppeteer both support font subsetting.