Skip to content
Flatten PDF Forms

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

CallScope
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.

Last updated on