Contents
Overview
This article is a comprehensive, practical tutorial that shows multiple ways to generate PDF files from PHP inside WordPress and force them to download by the browser. It covers library choices, installation, WordPress integration patterns (shortcodes, admin_post hooks, REST endpoints), security (nonces, capabilities), performance tips (streaming, X-Accel-Redirect), troubleshooting, and ready-to-use code examples you can drop into a plugin or a themes functions.php. All example code blocks use EnlighterJS style so you can copy them directly.
Quick summary
- Libraries covered: DOMPDF (HTML → PDF), FPDF / TCPDF / mPDF (programmatic or HTML with advanced features).
- Integration patterns: admin_post (recommended for link downloads), REST endpoint (for JS clients), direct shortcode link, AJAX (for small payloads).
- Key headers to force download: Content-Type: application/pdf, Content-Disposition: attachment filename=name.pdf, Content-Length (optional), and disable extra output.
- Security: use nonces and capability checks sanitize all input avoid sending PDFs with sensitive data to anonymous endpoints.
Choose a PDF library
Pick the library that matches your needs:
Library | When to use | Pros | Cons |
DOMPDF | Convert HTML/CSS to PDF for relatively simple pages | Easy HTML-based templates, widespread, composer-friendly | Limited advanced CSS support, memory-heavy for large pages |
mPDF | Advanced HTML/CSS rendering, better CSS support than DOMPDF | Good CSS and fonts support, works well with complex invoices | Heavy, larger memory/time requirements |
TCPDF / FPDF | Programmatic PDFs (precise layout, charts), low-level control | Stable, no Composer required for FPDF TCPDF has many features | Manual layout, steeper learning curve for complex HTML |
Installation
The recommended way is to install libraries using Composer in your plugin or via the theme if you control it. If you cannot use Composer, some libraries provide standalone distributions you can include.
- In a plugin root run: composer require dompdf/dompdf or composer require mpdf/mpdf.
- Include Composers autoload: require_once __DIR__ . /vendor/autoload.php
- If you must bundle the library manually, place it under your plugin folder and require the appropriate file, but prefer Composer to keep versions manageable.
WordPress integration patterns and best practices
- Do not output anything before sending PDF headers. Use admin_post or a REST endpoint so WordPress templates don’t emit partial HTML before headers.
- Use nonces and capability checks to prevent unauthorized access to private PDF data.
- Prefer server-side generation on request if content is dynamic optionally cache files in uploads if generation is expensive.
- Avoid debug output: disable WP_DEBUG display and ensure no trailing whitespace or BOM in PHP files.
- Memory timeout: adjust memory_limit and set_time_limit for large PDFs or use streaming/offload techniques like X-Sendfile.
Core technique: Forcing download with proper headers
The minimal approach that forces download is to:
- Generate or load the PDF binary string (or file on disk).
- Send headers:
- Content-Type: application/pdf
- Content-Disposition: attachment filename=your-file.pdf
- Content-Length: (optional, but helpful)
- Pragma: public, Cache-Control as appropriate
- Echo the content and call exit to prevent WP from appending HTML.
Example 1 — DOMPDF (HTML → PDF) admin_post download handler
Use this pattern when you want a link that triggers a download and the PDF is built from HTML (template or post content). The shortcode outputs a secured link to admin-post.php. The handler generates and streams the PDF.
1) Add a shortcode that prints a download link:
document.pdf, post_id => 0, ), atts, pdf_download_link) nonce = wp_create_nonce(generate_pdf_ . intval(atts[post_id])) url = add_query_arg(array( action => generate_pdf, post_id => intval(atts[post_id]), filename => sanitize_file_name(atts[filename]), nonce => nonce, ), admin_url(admin-post.php)) return . esc_html(Download PDF) . } ?>
2) Add admin_post handlers to generate and stream the PDF (DOMPDF example):
403)) } // Optional capability check for private data // if (post_id !current_user_can(read_post, post_id)) { wp_die(Unauthorized, , 403) } // Build HTML (use templates or full buffering) post = get_post(post_id) title = post ? apply_filters(the_title, post->post_title) : Document content = post ? apply_filters(the_content, post->post_content) :No content
html = html .=. esc_html(title) .
html .= content html .= // Generate PDF with DOMPDF dompdf = new Dompdf() dompdf->loadHtml(html) dompdf->setPaper(A4, portrait) dompdf->render() output = dompdf->output() // Clear buffer and send headers if (ob_get_length()) { ob_end_clean() } header(Content-Type: application/pdf) header(Content-Disposition: attachment filename= . filename . ) header(Content-Length: . strlen(output)) echo output exit } ?>
Example 2 — FPDF (programmatic PDF) and direct streaming
Use FPDF or TCPDF if you need precise control over positions, fonts, or generate programmatic invoices/reports.
AddPage() pdf->SetFont(Arial, B, 16) pdf->Cell(40, 10, Hello World!) // Output into string pdf_content = pdf->Output(, S) // S returns string if (ob_get_length()) { ob_end_clean() } header(Content-Type: application/pdf) header(Content-Disposition: attachment filename= . filename . ) header(Content-Length: . strlen(pdf_content)) echo pdf_content exit } ?>
Example 3 — Register a REST endpoint that returns a PDF (for SPA or AJAX)
If your frontend is React/Vue and you want to request a PDF via fetch and trigger a download, register a REST route and return the PDF binary response with proper headers. Use permission_callback and nonce or JWT as appropriate.
GET, callback => my_rest_pdf_callback, permission_callback => function() { return current_user_can(read) // adjust as needed }, )) }) function my_rest_pdf_callback(WP_REST_Request request) { html =REST PDF
Generated via REST
dompdf = new Dompdf() dompdf->loadHtml(html) dompdf->setPaper(A4) dompdf->render() pdf = dompdf->output() if (ob_get_length()) { ob_end_clean() } // Send headers manually — using REST responses with raw headers is tricky, but you can echo and exit header(Content-Type: application/pdf) header(Content-Disposition: attachment filename=rest.pdf) header(Content-Length: . strlen(pdf)) echo pdf exit } ?>
Note: The WP_REST_Response class is primarily for JSON when streaming binary responses from REST callbacks you will generally echo and exit to ensure headers are respected. Alternatively, return a redirect to an admin_post handler that streams PDF.
Saving to disk vs streaming
Options:
- Stream directly: Generate the PDF in memory and echo it with headers. Use for small to moderate PDFs and when you dont need to keep files.
- Save to uploads and redirect: Save PDF to wp_upload_dir() and then use X-Sendfile / X-Accel-Redirect or readfile() to send it. Good for large files and caching.
Example: save to uploads and then readfile (simple):
Better for high-load: configure your server to use X-Accel-Redirect (Nginx) or X-Sendfile (Apache/Lighttpd) to let the webserver serve the file while PHP only sends the header that points to the internal location. This offloads disk IO from PHP.
X-Accel-Redirect example (Nginx)
After saving to a protected internal location, reply with:
Then configure Nginx to map /protected_downloads/ to the internal filesystem path. This requires server config and is ideal for large files.
Common pitfalls and troubleshooting
- Blank or corrupt PDF: Check for extra whitespace or UTF-8 BOM in PHP files ensure no HTML is printed before headers disable WP_DEBUG display or log to file instead.
- Headers already sent: Use admin_post or REST handler and ensure no theme code runs before your handler. Use ob_end_clean() prior to sending headers.
- Large memory usage: Increase memory_limit or split generation use streaming and temporary files prefer X-Accel-Redirect to let the webserver serve the blob.
- Images not loading in PDF: Use absolute paths or base64-encoded images DOMPDF cannot fetch resources behind protected URLs unless configured.
- Fonts or CSS render incorrectly: Ensure fonts are available and referenced correctly by the PDF engine. For DOMPDF and mPDF you often need to register fonts.
Security checklist
- Always sanitize input (sanitize_text_field, sanitize_file_name, intval).
- Use nonces for one-time links: wp_create_nonce / wp_verify_nonce.
- Check capabilities (current_user_can) if PDFs contain private data.
- Store sensitive PDFs in non-public folders if saved on disk and use X-Sendfile/X-Accel-Redirect to serve after authorization.
- Limit generation frequency to mitigate abuse (rate limiting, require login, or add throttling).
Performance tips
- Cache generated PDFs when content doesnt change to avoid regenerating on every request.
- Use asynchronous background generation for large documents (WP Cron, queue worker) and give users a download link when ready.
- Offload file serving to the webserver (X-Accel-Redirect / X-Sendfile) or to object storage (S3) and provide pre-signed URLs for downloads.
- Minimize DOMPDF/mPDF memory usage by simplifying the HTML/CSS and removing unnecessary images/fonts.
Examples of real-world use cases
- Invoice generation from order data: programmatic PDF (TCPDF/FPDF) or HTML template (DOMPDF).
- Exporting posts/pages to PDF: convert the post content to HTML then to PDF via DOMPDF or mPDF.
- Protected report downloads for paid users: generate and store in a protected uploads folder serve via X-Sendfile after capability checks.
- On-the-fly certificates for users: generate programmatically and force immediate download.
Additional resources
- DOMPDF GitHub
- mPDF Documentation
- FPDF Homepage
- TCPDF Homepage
- wp_create_nonce() and wp_verify_nonce()
Final checklist before deploy
- Test PDF generation for a variety of content lengths and images.
- Verify headers are correct and no WP HTML leaks into the response.
- Confirm permissions and nonces block unauthorized users.
- Measure memory/time and implement X-Sendfile or save-to-disk if needed.
- Set appropriate cache headers or remove caching for private PDFs.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |