File & PDF Tools

Flatten PDF Forms

Bake PDF form fields into page content to prevent further editing.

Flatten PDF forms
Bake form field appearances into page content to prevent further editing.
Local browser processing

Flatten PDF Forms

Bake current AcroForm field appearances into page content so fields generally cannot be edited afterward.

Before Processing

  • Complete all fields first
  • Flattening is not a digital signature or permission control
  • XFA and other nonstandard forms may not work

Flatten PDF forms turns every AcroForm field appearance into page content and deletes the field objects, so filled values stop behaving like form data and become ordinary graphics. Use it for contracts and applications that are finished being filled and must render the same in every viewer and printer.

What a flattened field becomes on the page

Flattening paints nothing new. For each widget it takes the appearance stream already stored in the widget /AP /N entry, registers it in the page /Resources /XObject dictionary under a generated name such as /FlatWidget-7098480789, then appends a q block to the page content stream that sets 1 0 0 1 71.5 699.5 cm and runs /FlatWidget-7098480789 Do before the closing Q. That matrix is the widget /Rect origin taken verbatim, with no scaling or fitting; the BBox of the new XObject equals the /Rect size. Afterwards the /Fields array, the /Widget annotations and the catalog /AcroForm entry are removed. The XObject still holds text and vector operators, so the value stays sharp at any zoom and costs a few bytes, not a raster copy.

Existing appearances are reused, missing ones are redrawn

The flatten call first runs an appearance update, but that only redraws a field when it is marked dirty or when a widget has no /AP /N stream at all. A file that was filled and saved by Acrobat or another library therefore keeps exactly the appearance it shipped with: embedded subset font, border, /MK background. When /AP is missing, usually because the producer wrote only /V and relied on /NeedAppearances, pdf-lib draws the text itself with Helvetica, a WinAnsi font. A non-Latin value then kills the run with WinAnsi cannot encode "世" (0x4e16), printed verbatim in the panel, while ASCII values are laid out again at the size taken from /DA, so spacing can differ from what the viewer showed.

XFA forms, other annotations, and what stays live

Reading the form deletes the /XFA entry after a console warning, because pdf-lib can neither parse nor write XFA. Dynamic XFA files usually expose zero AcroForm fields, so the counter reads 0 and the output is only the static page artwork, with the XFA package gone. The cleanup pass removes /Widget annotations and any non-dictionary entry, and leaves /Link, /Text and highlight annotations interactive. Field-level /AA scripts die with the field object, yet document-level JavaScript, /OpenAction and permission flags are untouched: flattening is not a security step, and re-serialising the file breaks existing byte-range signatures.

Worked example

Flattening a three-field job application

Input
One 612 by 792 page, 3,549 bytes, with three terminal fields: text applicant.name = Alice Chen at (72, 700, 240, 20), text applicant.date = 2026-10-22, checkbox applicant.agree checked
Output
3,636 bytes; /Annots deleted, catalog /AcroForm removed, three /Form XObjects added (BBox [0 0 241 21], [0 0 241 21], [0 0 19 19]), content stream gains 1 0 0 1 71.5 699.5 cm blocks

The field count falls from 3 to 0. Those rectangles are 241 by 21 because pdf-lib expanded the requested 240 by 20 box by half the default one point border per side when the fields were created, which is where 72 becomes 71.5 and 18 becomes 19. Every block reuses the appearance stream the field already had, so the file grows by 87 bytes instead of embedding a new image.

Limits and pitfalls

  • One PDF per run, 100 MB per file; the 250 MB total limit only matters for the merge tool. Encrypted or password-protected files are rejected while loading, before a single field is written.
  • Only terminal fields reachable through /Fields are baked. A /Widget annotation whose field cannot be resolved is removed by the cleanup pass with nothing drawn in its place, so a box can vanish from the page.
  • The field counter reports terminal fields, not widgets: one field placed on two pages reads 1 while producing two /FlatWidget XObjects.
  • Document-level JavaScript, /OpenAction and the permission bits survive flattening, and any editor can still draw on top; re-serialising also invalidates existing byte-range signatures.

Related tools

When it goes wrong

The first signature box kills the run: the panel shows "Unexpected N type: undefined" in English, with no page name and no hint which field it was.

CauseIn pdf-lib, PDFSignature.needsAppearancesUpdate() always returns false, so the appearance pass skips an unsigned signature widget. When flattening reaches it, getNormalAppearance() calls ensureAP(), which creates an empty /AP dictionary, finds no /N entry, and throws using the constructor name of an undefined value. friendlyFileError has no rule for that text, so the raw message reaches the panel.

FixDeal with signature fields before flattening: delete the field in Acrobat, or actually sign it so a /AP /N stream exists. There is no per-field switch in this panel — the button runs getFields() to the end.

A checkbox called agree stops everything with "Failed to extract appearance ref for: agree".

CauseCheckboxes and radio groups need /AP /N to be a dictionary keyed by state names. pdf-lib looks up the current /V name first, then falls back to /Off; if the producer wrote only the checked state, or names it differently from the /V value, both lookups miss, so what comes back is still the dictionary and the reference check fails. Text and choice fields never take that branch.

FixOpen the file in a reader that rewrites appearances, toggle the box off and on, and save — the export then carries both the state key and /Off. If it still fails, delete the field and put a static box plus a tick glyph on the page instead.

The error reads "Could not find page for PDFRef 128 0 R" although the page holding that box is obviously there.

CauseThe widget /P entry resolves to none of the pages, so pdf-lib falls back to scanning every page /Annots for that reference. A file that was merged or reordered by a tool which copied /Fields but not the matching /Annots entries ends up with widgets no page points at.

FixRe-save the file in Acrobat once, which re-attaches /P and /Annots, or rewrite the page tree outside the browser, for example qpdf --qdf --object-streams=disable in.pdf out.pdf, then upload again. This tool has no repair path.

After the download the panel still says 3 form fields, and the properties dialog still shows the same modified date — did anything actually happen?

CauseThe counter is read once inside selectFiles() from the input document; the result is only a Blob in memory and is never parsed again. The input is also opened with { updateMetadata: false }, so /Producer and /ModDate are carried through the rewrite untouched.

FixDrop the downloaded *-flatten.pdf back into the drop zone. That fresh read comes from the new file’s /AcroForm, so 0 fields is the confirmation; anything above 0 means some field was not reachable through /Fields.

Typing 3-5 as the page range changes nothing — in this mode there is no page-range input at all.

CauseneedsPageRange lists only split, remove, rotate, crop and watermark, so the field is not rendered, and the flatten branch assigns selected = source.getPageIndices() — every page — ignoring the 1-N value the loader wrote into the model.

FixFor a partial job, extract those pages with the PDF split tool, flatten the extract, then merge the parts back. For a single page, printing that page to PDF is shorter than the split-flatten-merge round trip.

The flattened copy is a few hundred bytes larger than the original even though no page looks different.

CauseFlattening alone adds almost nothing: it turns each page /Contents from one stream into an array of [original, appended] and stores the appended q … cm … Do … Q block as an uncompressed stream. save({ useObjectStreams: true }) then repacks dictionary objects into compressed object streams, so the two effects cancel unevenly and the total moves either way.

FixJudge by behaviour, not size: click a field in the download and it should no longer take focus. If the file has to be smaller, use a dedicated PDF compressor or render pages to images; flattening is not a compression step.

One concrete scenario

Locking a 32-page onboarding packet before it goes to HR

The packet arrives as 32 pages and 2,097,152 bytes — exactly 2 MB, fifty times under the 104,857,600-byte cap, so size will not be what stops you. The panel reports 96 fields, meaning 96 widget appearances have to exist before a single one can be drawn, and 8 of them are unsigned signature boxes with no /AP /N. The run dies on the first of those, so fill or delete all 8, flatten the remaining 88, then drop the output back in and require a counter of 0. If only pages 12-14 must be printable, split them out first: this mode reads every page index and offers no page range.

Frequently Asked Questions

Flattening versus printing to PDF?
Flattening bakes AcroForm field appearances into the content stream while keeping text vector and sharp; printing can rasterize. When fields must become non-fillable but stay crisp, flatten.
Can fields still be edited after flattening?
Generally no — field objects are removed and their appearance becomes page content. That is the point: finalized contracts and submitted applications. Complete every field first.
Is flattening an electronic signature?
No. Flattening freezes appearance but provides no identity binding or tamper evidence; legal signatures require certificates or e-signature platforms. Use flattening as the final "lock-in" step of a signing flow, not as the signature.

Content reviewed: