Flatten PDF Forms
Flattening takes each widget annotation’s appearance stream, draws it into the page content stream as an XObject, and drops the annotation from the page. The result looks the same but the fields are gone: no editing, no form data to export, and the text is now ordinary page content that text extraction will pick up. It is the usual last step before archiving or sending a completed form out.
Flattening is destructive and there is no way back within the same document, so run it on a copy.
Which call to use
| Call | Scope |
|---|---|
reader.FlattenFields(false, appgen) | All form fields. Other annotations left alone. |
reader.FlattenFields(true, appgen) | All form fields and every other annotation on every page. |
reader.FlattenFieldsWithOpts(appgen, opts) | Only the fields opts.FilterFunc selects. |
page.FlattenFieldsWithOpts(appgen, opts) | One page. Widgets always, other annotations per opts.AnnotFilterFunc. |
pdfReader, err := model.NewPdfReader(f)
if err != nil {
return err
}
fieldAppearance := annotator.FieldAppearance{OnlyIfMissing: true}
if err := pdfReader.FlattenFields(true, fieldAppearance); err != nil {
return err
}
pdfWriter, err := pdfReader.ToWriter(&model.ReaderToWriterOpts{SkipAcroForm: true})
if err != nil {
return err
}
return pdfWriter.WriteToFile(outputPath)SkipAcroForm: true on the writer is belt and braces. FlattenFields already
sets reader.AcroForm to nil when it flattened everything, so there would be
nothing to copy, but the flag makes the intent explicit and covers the partial
case.
The appearance generator argument matters. Passing nil flattens whatever
appearance streams are already in the file and nothing else, which is fine for a
form filled in Acrobat but produces blank fields for one filled
programmatically. annotator.FieldAppearance{OnlyIfMissing: true} fills that gap
by generating an appearance for fields that lack one, leaving existing appearances
untouched. See
fill and flatten with appearance for the
options.
Flattening only some fields
FlattenFieldsWithOpts keeps the fields its filter rejects, and the AcroForm
survives with those fields still in it:
opts := model.FieldFlattenOpts{
FilterFunc: func(f *model.PdfField) bool {
return f.PartialName() == "signature_date"
},
}
err = pdfReader.FlattenFieldsWithOpts(annotator.FieldAppearance{}, &opts)Rejecting a non-terminal field discards its children too. A non-terminal field is kept in the AcroForm if any of its children were kept.
The page-level FlattenFieldsWithOpts behaves differently on one point: widget
annotations on that page are always flattened, and AnnotFilterFunc only decides
about the non-widget annotations. FilterFunc is not consulted there, because a
page has no AcroForm context.
Limitations
A widget with no appearance stream draws nothing. The annotation is still removed, so you end up with an empty space where the value was. If the field has a value, this is logged at debug level as an error; if it has no value, it is silently skipped, which is the correct outcome for an empty field. Generating appearances first is the fix.
Link annotations are dropped rather than drawn. With allannots true, popup,
link and projection annotations are removed from the page without anything being
written to the content stream, so URLs stop being clickable. Use
FlattenFieldsWithOpts with an AnnotFilterFunc that rejects
*model.PdfAnnotationLink if the links have to survive;
pdf_form_flatten_non_url.go
does exactly that.
Fonts from an appearance stream are copied into the page resources only when the page does not already have a font under the same resource name. A collision means the page’s existing font is used to draw the flattened text, which can shift the glyphs. Rare, but it explains a flattened field that renders in the wrong typeface.
Flattening a signature field invalidates the signature, since it rewrites the page content that the signature covers. Sign after flattening, not before.
Run the example
flattenPdf is the whole operation: read, flatten, write. The example takes an
output directory followed by one or more input files, and writes
flattened_<name>.pdf for each, reporting per-file failures at the end rather
than stopping on the first one.
git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/forms
go run pdf_form_flatten.go <OUTPUTDIR> <INPUT1.pdf> [INPUT2.pdf ...]If this is your first time using UniPDF, follow the getting started guide to create an API key and set up your development environment.
View the full source
Two related examples cover the partial cases:
pdf_form_partial_flatten.go
flattens a named subset of fields through FilterFunc, and
pdf_form_flatten_non_url.go
flattens everything except URL link annotations.