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
| Call | For |
|---|---|
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.pdfIf 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.