Skip to content

Merge PDFs

pdfutil.MergePdf concatenates a list of files into one document: every page of the first input, then every page of the second, and so on. It is a single call, and the only decision it asks you to make is whether to merge AcroForms.

err := pdfutil.MergePdf([]string{"input1.pdf", "input2.pdf"}, "output.pdf", true)

The third argument is isMergingForms. The godoc’s advice is that if you are not sure, set it to true; the example passes false because its inputs have no forms. Passing false when they do means the merged pages keep their widget annotations but the document loses the AcroForm that ties them together, so the fields stop working.

Which call to use

CallFor
MergePdf(inputPaths, outputPath, isMergingForms)Files on disk.
MergePdfWithFilePermission(..., perm)Same, with a file mode other than 0644.
MergePdfStreams(inputs []io.ReaderAt, output io.Writer, isMergingForms)Sources already in memory, or writing to a response.
MergePdfWithOptions(inputPaths, outputPath, opts)When you need the optimizer.
MergePdfStreamsWithOptions(inputs, output, opts)Both of the above at once.

The stream forms take io.ReaderAt, not io.ReadSeeker. *os.File, *bytes.Reader and *io.SectionReader all qualify; a bytes.Buffer or a network stream does not, so wrap those in bytes.NewReader(b) after reading them into memory. Byte length is detected from the source, so there is no size argument.

MergePdfOptions has two fields, IsMergingForms and Optimizer:

err := pdfutil.MergePdfWithOptions(inputPaths, outputPath, pdfutil.MergePdfOptions{
    IsMergingForms: true,
    Optimizer: optimize.New(optimize.Options{CombineDuplicateStreams: true}),
})

Limitations

Merging carries pages and, optionally, the AcroForm. Nothing else. Outlines, document info, XMP metadata, page labels, named destinations and viewer preferences all belong to the document rather than to a page, and the merge builds a fresh writer that never receives them. Bookmarks disappearing from a merged file is the usual first surprise.

Form merging renames fields. The first document’s fields keep their names, and each subsequent document’s fields are re-parented under a new non-terminal field called doc1, doc2 and so on, so a field named name from the second input becomes doc1.name in the output. Anything filling the merged form by fully qualified name has to account for that. The form’s own attributes, NeedAppearances, DA, Q and SigFlags, are taken from the first document that sets them, and XFA from the first document that has one; XFA is not merged.

No optimizer runs by default, so identical resources are written once per input. Merge twenty copies of a file with an embedded font and the font program lands in the output twenty times. CombineDuplicateStreams is what collapses them, at the cost of a pass over the whole document.

The file-based calls hold every input open until the merge returns and buffer the whole merged document in memory before writing. Hundreds of inputs will run into the open file limit; a few large ones will show up as memory use.

Inputs are opened with a nil ReaderOpts, which means an empty password. A file protected by an owner password only still merges, but one with a user password fails with unable to decrypt password protected file. Decrypt it separately first.

Run the example

The output path comes first on the command line, before the inputs. main reads it off os.Args[1], treats everything after it as an input, and calls MergePdf with isMergingForms set to false.

git clone https://github.com/unidoc/unipdf-examples.git
cd unipdf-examples/pages
go run pdf_merge.go output.pdf 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 sit in the same folder. pdf_merge_advanced.go is the same program with isMergingForms set to true, and pdf_merge_with_page_numbers.go merges through creator instead so it can draw a footer on each page.

Last updated on