# PDF Generation: Setup and Troubleshooting

> FormForge PDF Generation turns each submission into a branded PDF attached to notification emails: setup, non-Latin fonts, storage and four failure modes.

- Canonical: <https://royalplugins.com/support/formforge/pdf-generation/>
- Last updated: 2026-09-21
- HTML version: <https://royalplugins.com/support/formforge/pdf-generation/>

---

Turn every form submission into a branded PDF and (optionally) attach it to the notification email. This page walks through the two-checkbox setup, what the generated PDF looks like, where files land, how non-Latin scripts are handled, and the four failure modes that cover almost every PDF-related support ticket.

Shortest path to a working PDF

Open the form in the builder, open **Form Settings**, find the **PDF Generation** section, check **Generate PDF of submission** and (optionally) **Attach PDF to notification email**. Save the form. Submit a test entry. A PDF lands in `wp-content/uploads/formforge/pdf/` and (if you checked the attach box) arrives as an attachment on the notification email.

## How PDF generation works

FormForge Pro uses the mPDF library to render each submission as a PDF document. The PDF layout comes from a built-in template that reads the form’s field labels and values, wraps them in a clean two-column table with a header (optional site logo, form title, accent color bar) and a footer (submission date, entry ID, custom footer text), and outputs to `wp-content/uploads/formforge/pdf/` with a unique filename per entry.

The generated PDF surfaces one main way for now:

- **Notification email attachment**: when Attach PDF to notification email is checked, the PDF gets attached to every notification the form sends out. Recipients click the attachment in their mail client and open the PDF directly.
- **Developer download endpoint**: an AJAX endpoint at `admin-ajax.php?action=rfp_download_pdf&entry_id=X` re-generates the PDF for any entry on demand. Admin users can hit it directly; a security token (see [developer hooks](#developer-hooks)) lets you surface a public download link to a submitter without exposing the AJAX endpoint to anyone else. There’s no built-in Download button in the Entries admin yet, so today the attach-to-notification-email path is the primary way both you and your customers see the PDF.

## Enable PDF generation on a form

#### Confirm your license tier is Business or higher

PDF Generation is a Business-tier feature. Pro-tier and Personal-tier licenses can toggle the checkboxes in the form settings, but the actual PDF generation code doesn’t load, so nothing happens on submit. Upgrade at [FormForge pricing](https://royalplugins.com/formforge/pricing/) if needed.

#### Confirm the mPDF library is installed

Go to **FormForge > Settings** and look for the PDF status line. If it reads *PDF generation is ready*, you’re set. If it reads *PDF generation requires the mPDF library. Please run “composer install” in the plugin directory or contact support*, the library ships with FormForge Pro but wasn’t installed for your deployment; see the troubleshooting section below.

#### Open the form in the builder

Go to **FormForge > All Forms** and click the form name to open the drag-and-drop builder.

#### In the Form Settings panel, click the Advanced tab

The Form Settings panel is on the right side of the builder and is visible by default (whenever you’re not editing a specific field). Along the top of the panel are five tabs: General, Notifications, Confirmation, Integrations, and Advanced. Click **Advanced** and scroll to the **PDF Generation** section.

#### Check “Generate PDF of submission”

Enables auto-generation on every form submission. The PDF is written to `wp-content/uploads/formforge/pdf/` immediately after the entry saves.

#### Optionally check “Attach PDF to notification email”

If you also want the PDF to arrive as an email attachment on every outgoing notification, check this box too. The two checkboxes are independent: you can generate PDFs without attaching them (admin download only), or attach without generating (not useful, since there’s nothing to attach). The normal setup is both boxes checked.

#### Save the form

Click **Save Form** at the top right of the builder. Submit a test entry to confirm the PDF arrives.

## What the generated PDF looks like

The default template renders as a portrait-orientation A4 page with three regions:

| Region | Contents |
| --- | --- |
| **Header** | Optional logo (top-left, max 180px wide, 60px tall). Form title in the accent color. Accent-color bar underlines the header. |
| **Body** | Two-column table of field labels (left) and submitted values (right). Every non-empty field appears in order. File uploads render the filename. Star ratings render as filled and empty star characters plus the numeric score. NPS scores render with the Promoter / Passive / Detractor category. URL and email values render as clickable links. Layout-only fields (Section, HTML, Page Break) and hidden fields are skipped. |
| **Footer** | Submission date, entry ID, and optional custom footer text. Small font, muted color. |

Defaults you can currently override

The form-settings UI exposes just the two enable checkboxes today. Every other PDF setting (page size, orientation, accent color, header logo URL, custom footer text, PDF password protection) has a sensible default baked in but is only tunable via the [filter hooks](#developer-hooks) below. Defaults: A4 portrait, accent color `#2271b1`, no logo, no password, submission date and entry ID in the footer.

## Where PDFs are stored and how long they last

Every generated PDF lives at:

`wp-content/uploads/formforge/pdf/form-{form_id}-entry-{entry_id}-{random}.pdf`

The random suffix is eight alphanumeric characters, so the exact filename isn’t guessable by anyone who happens to know the form and entry IDs. FormForge also drops `.htaccess` and `index.php` files into the directory on first use, so the files can’t be enumerated via directory listing.

A daily cleanup job removes any PDF older than one hour from the working directory. That’s intentional: PDFs are always re-generatable from the entry data via the AJAX download endpoint, so the on-disk copy is treated as a short-lived cache rather than a long-term archive. If you need long-term archival, either attach the PDF to the notification email at generation time (email inbox becomes the archive), or hit the download endpoint and store the file wherever fits your retention policy.

The download endpoint regenerates on demand

Hitting the download endpoint for an entry re-runs mPDF against the current entry data plus the current form settings. That means if you change the accent color or logo in the form’s PDF filter hooks after entries have been submitted, older entries will download with the new branding, not the branding at the time of submission. This is usually desirable (consistent branding across old submissions) but if you need historic-branding fidelity, snapshot the PDF at generation time by attaching it to the notification email.

## Handling non-Latin scripts (Arabic, Chinese, Japanese, Cyrillic, etc.)

mPDF renders each character by looking up its Unicode code point in the currently-selected font. If the font doesn’t include a glyph for a given character, the PDF shows a box or a question mark in that position.

FormForge ships with the DejaVu Sans font family as the default, which covers:

- Latin (English, Spanish, French, German, Italian, Portuguese, and most European languages)
- Greek
- Cyrillic (Russian, Ukrainian, Bulgarian, Serbian, and other Cyrillic-script languages)
- Armenian, Georgian
- Basic diacritics and math symbols

If your form collects submissions in scripts outside DejaVu’s coverage (Arabic, Hebrew, Thai, Chinese, Japanese, Korean, Devanagari), you’ll see boxes. The fix has two parts:

#### Drop a compatible font file into the FormForge fonts directory

The directory is `wp-content/uploads/formforge/pdf-fonts/`. Any `.ttf` or `.otf` font file placed there is discoverable by mPDF. For CJK (Chinese / Japanese / Korean), Google’s Noto CJK family is a common free choice. For Arabic and Hebrew, Amiri and Frank Ruehl are good starting points.

#### Register the font with mPDF via the `rfp_mpdf_config` filter

Add a small snippet in your theme’s `functions.php` or a custom plugin:

`add_filter( 'rfp_mpdf_config', function( $config ) {
$config['fontdata']['notosans'] = array(
'R' => 'NotoSansCJK-Regular.ttf',
'B' => 'NotoSansCJK-Bold.ttf',
);
return $config;
} );`

The `R` and `B` keys are Regular and Bold weights. Add `I` for italic and `BI` for bold-italic if you have those files too.

#### mPDF’s autoScriptToLang picks it up automatically

FormForge’s default mPDF config enables `autoScriptToLang` and `autoLangToFont`, so mPDF will automatically switch to your registered font for runs of non-Latin text within the same PDF. You don’t need to set the default font manually.

Test with a single form field before you go global

Set up a test form with one text field, submit a value in the non-Latin script you care about, and generate the PDF. If characters render correctly there, they’ll render correctly in every future submission on any form.

## Troubleshooting the four common failures

### 1. “PDF generation requires the mPDF library”

The FormForge Pro zip should ship with the mPDF library pre-bundled in the `vendor/` directory, so you shouldn’t normally see this error. If you do: your host or deployment process is stripping the vendor directory during install (common on git-based deploys and Bedrock-style setups that `.gitignore` vendor/). Fix: run `composer install` inside the `wp-content/plugins/formforge-pro/` directory, or ask your host to include vendor/ in the deployment. Confirm with **FormForge > Settings** showing “PDF generation is ready.”

### 2. “Could not create PDF directories”

The web server doesn’t have write permission on `wp-content/uploads/`. This is a general WordPress issue, not a FormForge-specific one; the WordPress Media Library uploads would also fail. Fix: contact your host to fix the permissions on `wp-content/uploads/`, or if you have SSH access, run `chmod 755 wp-content/uploads/` and confirm the WordPress admin user (usually `www-data`, `apache`, or your PHP-FPM pool user) owns the directory.

### 3. Non-Latin characters render as boxes or question marks

Font coverage issue. See the [non-Latin scripts section](#fonts) above for the two-step fix: drop the font file into `wp-content/uploads/formforge/pdf-fonts/`, register it via `rfp_mpdf_config`. mPDF picks it up automatically after that.

### 4. PDF generation is very slow, or fails with a timeout

mPDF is memory-hungry, and forms with many fields, embedded images, or non-Latin script (which triggers additional font loading) can push memory usage past PHP’s limits. Two levers:

- **Raise `memory_limit` to 256M** (or 512M for very large forms with images). Check your current value in **Tools > Site Health > Info** under Server. Raise via `php.ini`, `.htaccess`, or your host’s control panel.
- **Raise `max_execution_time` to 60 seconds** for the same reason. Default 30 seconds is usually enough for simple forms but not for anything with multiple images or long non-Latin text runs.

If simple forms without images or non-Latin script are still slow, that’s abnormal. Enable `WP_DEBUG_LOG`, submit a test entry, then check `wp-content/debug.log` for entries prefixed `FormForge Pro PDF Error:` which surface the exact mPDF exception. Send that to [support](https://royalplugins.com/contact/) for help.

Still stuck after all four checks?

Gather: (a) the exact status shown in **FormForge > Settings** for PDF availability, (b) the PDF Generation section settings from the form, (c) contents of `wp-content/debug.log` filtered for lines starting with “FormForge Pro PDF Error”, (d) whether other forms on the same site can generate PDFs correctly. Send to [support](https://royalplugins.com/contact/) and we can dig in.

## Developer hooks

Three filter and action hooks let you customize the PDF output beyond what the Form Settings UI exposes.

### `rfp_mpdf_config` filter

Runs before the mPDF instance is created. Lets you override any mPDF configuration option (page size, margins, default font, fontdata registration, PDF/A compliance mode, custom stylesheets, and more). Signature: `apply_filters( 'rfp_mpdf_config', $config, $entry_id, $form_id, $settings )`.

`add_filter( 'rfp_mpdf_config', function( $config, $entry_id, $form_id, $settings ) {
$config['margin_top'] = 25;
$config['margin_bottom'] = 20;
return $config;
}, 10, 4 );`

### `rfp_mpdf_before_write` action

Fires with the mPDF instance immediately before the main HTML body is written to it. Use for cover pages, watermarks, custom fonts, or SetHeader / SetFooter overrides. Signature: `do_action( 'rfp_mpdf_before_write', $mpdf, $entry_id, $form_id, $settings )`.

`add_action( 'rfp_mpdf_before_write', function( $mpdf, $entry_id, $form_id, $settings ) {
$mpdf->SetWatermarkText( 'CONFIDENTIAL' );
$mpdf->showWatermarkText = true;
}, 10, 4 );`

### `rfp_mpdf_after_write` action

Fires with the mPDF instance immediately after the main HTML body is written. Use for appending signature pages, terms and conditions, quiz results, or any additional content that should come after the field-value table. Signature: `do_action( 'rfp_mpdf_after_write', $mpdf, $entry_id, $form_id, $settings )`.

All three hooks receive the same four arguments

Every hook is called with the mPDF instance (or config array), the entry ID, the form ID, and the PDF settings array for the form. That gives you enough context to conditionally customize the PDF based on which form or which entry is being generated. For example, only add a watermark to forms with a specific ID, or only use CJK fonts if the entry data contains CJK characters.

## FAQ

### How do I enable PDF generation on a FormForge Pro form?

PDF Generation is a Business-tier feature. Open the form in the builder, open **Form Settings**, find the **PDF Generation** section, check **Generate PDF of submission** to auto-create a PDF for every entry. Optionally check **Attach PDF to notification email** to include the PDF as an attachment in outgoing notifications. Save the form.

### Where do generated PDFs get saved?

Every generated PDF is written to `wp-content/uploads/formforge/pdf/` with a filename like `form-42-entry-158-a1b2c3d4.pdf`. The directory is protected by an auto-generated `.htaccess` with “deny from all” plus an empty `index.php` so files aren’t listable directly. An AJAX endpoint at `admin-ajax.php?action=rfp_download_pdf` handles authenticated admin downloads and, with a security token, public-share download links.

### How long are generated PDFs kept?

A daily cleanup job removes any PDF older than one hour from the working directory and the mPDF temp directory. If a customer needs the PDF long-term, either attach it to the notification email at generation time (email inbox becomes the archive), or hit the AJAX download endpoint and archive the file yourself. The endpoint regenerates the PDF from entry data on demand, so cleanup deletes the cached file, not the ability to recover a copy.

### The PDF shows boxes instead of Arabic / Chinese / Japanese / Cyrillic characters. Why?

The mPDF library needs the appropriate font file to render each script. FormForge ships with the DejaVu Sans font family which covers Latin, Greek, Cyrillic, Armenian, Georgian, and a few others. Scripts outside DejaVu’s coverage (Arabic, Hebrew, Thai, Chinese, Japanese, Korean, Devanagari) require additional font files. Drop the font file (a `.ttf` file) into `wp-content/uploads/formforge/pdf-fonts/` and use the `rfp_mpdf_config` filter to register it under `fontdata`. mPDF’s `autoScriptToLang` setting will then automatically pick it up for any non-Latin runs in the submitted data. Full walkthrough in the [non-Latin scripts section](#fonts).

### I enabled PDF generation but no PDF appears. Why?

Four things to check in order. (1) Is your license tier at Business or higher? PDF generation is gated at Business. (2) Is the mPDF library installed? Check **FormForge > Settings** for a “PDF generation is ready” status; if it reads “PDF generation requires the mPDF library,” run `composer install` in the plugin directory or contact support. (3) Is `wp-content/uploads/` writable by the web server? If not, PDF creation fails silently with “Could not create PDF directories.” (4) Check `wp-content/debug.log` for entries prefixed “FormForge Pro PDF Error:” which surface the exact mPDF exception.

### Can I customize the PDF template beyond the default header / body / footer?

Yes, via three filter and action hooks: `rfp_mpdf_config` (customize the mPDF configuration array), `rfp_mpdf_before_write` (fires with the mPDF instance before the main HTML is written, useful for cover pages, watermarks, custom fonts), and `rfp_mpdf_after_write` (fires after the main HTML is written, useful for signatures, terms and conditions, or additional pages). See the [developer hooks section](#developer-hooks) for signatures and examples.

### Why is PDF generation slow?

mPDF is memory-intensive and can be slow on forms with many fields or large images. Two levers: (1) raise the PHP `memory_limit` to 256M (128M is usually not enough for PDFs with images) and `max_execution_time` to 60 seconds via `php.ini` or your host’s control panel. (2) Use the `rfp_mpdf_config` filter to disable expensive features like `autoScriptToLang` if your forms only ever contain Latin characters. Truly slow generation is worth investigating; if you’re seeing multi-second delays on simple forms, contact [support](https://royalplugins.com/contact/).

### Can I use PDF generation with multi-page forms?

Yes. The PDF template renders every field the visitor submitted regardless of which form page they were on. Page Break fields themselves don’t appear in the PDF (they’re layout markers, not data). The PDF is a flat, print-friendly summary of everything the visitor entered across all pages of a wizard.

[Submissions Not Saving](https://royalplugins.com/support/formforge/submissions-not-saving/)
[Stripe Integration](https://royalplugins.com/support/formforge/stripe-integration/)
