Merge PDF Documents

The PdfMerge class merges PDF documents, or selected pages of them, blank pages and images into a single PDF document. The form fields, bookmarks, annotations, internal links and scripts of the merged documents are kept. The merged documents can be password protected, the merged document can have its own document information, security options, viewer preferences, bookmarks, open action and digital signature and HTML templates can be drawn in all its pages, for example a footer with the page numbers.

The demo application merges a multi-page document with bookmarks and a form with fields, optionally with a blank page and an image page between them, adds a bookmark for each document and a footer with the page numbers and sets an open action.

Add Documents and Pages

PdfMergeAddPdf(Byte, String) and PdfMergeAddPdf(String, String) add all the pages of a document from memory or from a file, with its password when it is protected. PdfMergeAddPdfPages(Byte, Int32, Int32, String) adds a range of pages and PdfMergeAddPdfPages(Byte, String, String) the pages of several ranges, in the order of the ranges. The methods return the number of the first added page in the merged document, where a bookmark or the open action can point. PdfMergeAddBlankPage(PdfPageSize, PdfPageOrientation) adds a blank page and PdfMergeAddImagePage(String, PdfPageSize, PdfPageOrientation) a page displaying an image, with the size of the image or with the image fitted in a page of the given size; both return the number of the added page. PageCount is the number of pages of the merged document.

Merge Documents and Pages
using PdfMerge pdfMerge = new PdfMerge();

int pageCount = 0;
int firstDocumentPage = pageCount + 1;
pageCount += pdfMerge.AddPdf(firstPdfBytes);

// A blank page between the documents
pageCount = pdfMerge.AddBlankPage(PdfPageSize.A4, PdfPageOrientation.Portrait);

// Selected pages of the second document, in the order of the ranges
int secondDocumentPage = pageCount + 1;
pageCount += pdfMerge.AddPdfPages(secondPdfBytes, "1,3-", null);

byte[] mergedPdf = pdfMerge.Save();

Form Fields and Bookmarks

When the same document, or documents with fields of the same names, are merged, the fields with the same name become one field with the same value in all the copies. With KeepFieldsSeparate set to true they are renamed, so each copy keeps its own value. Flatten draws the fields and the annotations as static content.

The bookmarks of the merged documents are copied in the merged document when KeepSourceBookmarks is true, under the bookmarks added to Bookmarks with the page numbers of the merged document, as described in the Add Bookmarks and Open Action to Generated PDF topic.

Keep the Fields Separate and Add Bookmarks
pdfMerge.KeepFieldsSeparate = true;
pdfMerge.KeepSourceBookmarks = true;
pdfMerge.Bookmarks.Add("First document", firstDocumentPage, PdfLinkPageLocation.FitPage());
pdfMerge.Bookmarks.Add("Second document", secondDocumentPage, PdfLinkPageLocation.FitPage());

Properties of the Merged Document

PdfDocumentInfo, PdfSecurityOptions, PdfViewerPreferences and DigitalSignature apply to the merged document. OpenAction is the action executed by the viewer when the document is opened, for example a PdfGoToPageAction to the first page of the second document. A HTML template added with PdfMergeAddHtmlTemplate(Int32, Int32, Int32, Int32, String, String) is drawn in all the pages, with the {page_number} and {total_pages} placeholders replaced in each page. PdfMergeInfo reports the pages produced for each document after the save.

Set the Open Action and a Footer
pdfMerge.PdfDocumentInfo.Title = "Merged Document";
pdfMerge.OpenAction = new PdfGoToPageAction(secondDocumentPage, PdfLinkPageLocation.FitPage());

PdfHtmlTemplate footer = pdfMerge.AddHtmlTemplate(0, 0, 300, 30,
    "<div style=\"font-family: Arial; font-size: 10px;\">Page {page_number} of {total_pages}</div>", null);
footer.HorizontalAlign = PdfTemplateHorizontalAlign.Center;
footer.VerticalAlign = PdfTemplateVerticalAlign.Bottom;

Code Sample - Merge PDF Documents

C#
using System;
using System.IO;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using EvoPdf_Next_AspNetDemo.Models;
using EvoPdf_Next_AspNetDemo.Models.PDF_Editor;

// Use EVO PDF Namespace
using EvoPdf.Next;

namespace EvoPdf_Next_AspNetDemo.Controllers.PDF_Editor
{
    public class Merge_PDF_DocumentsController : Controller
    {
        private readonly IWebHostEnvironment m_hostingEnvironment;

        public Merge_PDF_DocumentsController(IWebHostEnvironment hostingEnvironment)
        {
            m_hostingEnvironment = hostingEnvironment;
        }

        public IActionResult Index()
        {
            var model = SetViewModel();
            return View(model);
        }

        [HttpPost]
        public async Task<IActionResult> MergePdf(Merge_PDF_Documents_ViewModel model)
        {
            if (!ModelState.IsValid)
            {
                var errorMessage = ModelStateHelper.GetModelErrors(ModelState);
                throw new ValidationException(errorMessage);
            }

            // Set the license key received after purchase to use the library in licensed mode; leave it commented for demo mode
            // Licensing.LicenseKey = "your-license-key";

            byte[] firstPdfBytes = await ReadInputPdf(model.FirstPdfFile, model.FirstPdfFileUrl);
            byte[] secondPdfBytes = await ReadInputPdf(model.SecondPdfFile, model.SecondPdfFileUrl);
            string firstPassword = string.IsNullOrEmpty(model.FirstPassword) ? null : model.FirstPassword;
            string secondPassword = string.IsNullOrEmpty(model.SecondPassword) ? null : model.SecondPassword;

            using PdfMerge pdfMerge = new PdfMerge();

            // The bookmarks of the merged documents are copied in the merged document, under the bookmarks added below
            pdfMerge.KeepSourceBookmarks = model.KeepSourceBookmarks;

            // The fields with the same name in several merged documents are renamed, so each copy keeps its own value;
            // otherwise they become one field with the same value in all the copies
            pdfMerge.KeepFieldsSeparate = model.KeepFieldsSeparate;

            // The form fields and annotations of the merged document become static content
            pdfMerge.Flatten = model.FlattenMergedPdf;

            pdfMerge.PdfDocumentInfo.Title = model.DocumentTitle;

            // The first document, all its pages or the pages of the given ranges. The add methods return the number
            // of the first added page in the merged document
            int firstDocumentPage = AddPdf(pdfMerge, firstPdfBytes, model.FirstPageRanges, firstPassword);
            AddBookmark(pdfMerge, model.AddDocumentBookmarks, "First document", firstDocumentPage);

            if (model.AddBlankPage)
            {
                // A blank page of the given size between the documents
                int blankPage = pdfMerge.AddBlankPage(SelectedPdfPageSize(model.BlankPageSize), SelectedPdfPageOrientation(model.BlankPageOrientation));
                AddBookmark(pdfMerge, model.AddDocumentBookmarks, "Blank page", blankPage);
            }

            if (model.AddImagePage)
            {
                // A page displaying an image: the page has the size of the image, or the image is fitted in a page of the given size
                string imagePath = Path.Combine(GetDemoFilesPath(), "Image_Files", "image.jpg");
                int imagePage = model.FitImageInPage
                    ? pdfMerge.AddImagePage(imagePath, PdfPageSize.A4, PdfPageOrientation.Portrait)
                    : pdfMerge.AddImagePage(imagePath);
                AddBookmark(pdfMerge, model.AddDocumentBookmarks, "Image page", imagePage);
            }

            int secondDocumentPage = AddPdf(pdfMerge, secondPdfBytes, model.SecondPageRanges, secondPassword);
            AddBookmark(pdfMerge, model.AddDocumentBookmarks, "Second document", secondDocumentPage);

            if (model.AddSecondPdfTwice)
            {
                int copyPage = AddPdf(pdfMerge, secondPdfBytes, model.SecondPageRanges, secondPassword);
                AddBookmark(pdfMerge, model.AddDocumentBookmarks, "Second document, copy", copyPage);
            }

            if (model.AddHeaderFooter)
            {
                // A template is drawn in all the pages of the merged document; the page number variables are replaced in each page
                PdfHtmlTemplate footer = pdfMerge.AddHtmlTemplate(0, 0, 300, 30,
                    "<div style=\"font-family: Arial; font-size: 10px; color: #606060;\">Merged document, page {page_number} of {total_pages}</div>", null);
                footer.HorizontalAlign = PdfTemplateHorizontalAlign.Center;
                footer.VerticalAlign = PdfTemplateVerticalAlign.Bottom;
            }

            switch (model.OpenAction)
            {
                case "SecondDocument":
                    // The merged document opens at the first page of the second document
                    pdfMerge.OpenAction = new PdfGoToPageAction(secondDocumentPage, PdfLinkPageLocation.FitPage());
                    break;
                case "LastPage":
                    pdfMerge.OpenAction = new PdfGoToPageAction(pdfMerge.PageCount, PdfLinkPageLocation.FitWidth(0));
                    break;
                case "JavaScript":
                    pdfMerge.OpenAction = new PdfJavaScriptAction("app.alert('The merged document has " + pdfMerge.PageCount + " pages');");
                    break;
            }

            byte[] outPdfBuffer = pdfMerge.Save();

            FileResult fileResult = new FileContentResult(outPdfBuffer, "application/pdf");
            fileResult.FileDownloadName = "Merge_PDF_Documents.pdf";
            return fileResult;
        }

        // Adds all the pages of a document, or the pages of the given ranges, and returns the number of the first
        // added page in the merged document
        private static int AddPdf(PdfMerge pdfMerge, byte[] pdfBytes, string pageRanges, string password)
        {
            if (string.IsNullOrWhiteSpace(pageRanges))
                return pdfMerge.AddPdf(pdfBytes, password);

            return pdfMerge.AddPdfPages(pdfBytes, pageRanges.Trim(), password);
        }

        private static void AddBookmark(PdfMerge pdfMerge, bool addBookmarks, string title, int pageNumber)
        {
            if (addBookmarks)
                pdfMerge.Bookmarks.Add(title, pageNumber, PdfLinkPageLocation.FitPage());
        }

        private static PdfPageSize SelectedPdfPageSize(string selectedValue)
        {
            switch (selectedValue)
            {
                case "A3": return PdfPageSize.A3;
                case "A5": return PdfPageSize.A5;
                case "Letter": return PdfPageSize.Letter;
                case "Legal": return PdfPageSize.Legal;
                case "Ledger": return PdfPageSize.Ledger;
                default: return PdfPageSize.A4;
            }
        }

        private static PdfPageOrientation SelectedPdfPageOrientation(string selectedValue)
        {
            return selectedValue == "Landscape" ? PdfPageOrientation.Landscape : PdfPageOrientation.Portrait;
        }

        private async Task<byte[]> ReadInputPdf(IFormFile pdfFile, string pdfFileUrl)
        {
            // If an uploaded file exists, use it with priority
            if (pdfFile != null && pdfFile.Length > 0)
            {
                try
                {
                    using var ms = new MemoryStream();
                    await pdfFile.CopyToAsync(ms);
                    return ms.ToArray();
                }
                catch (Exception ex)
                {
                    throw new Exception("Failed to read the uploaded PDF file", ex);
                }
            }

            // Otherwise, fall back to the URL
            string pdfUrl = pdfFileUrl?.Trim();
            if (string.IsNullOrWhiteSpace(pdfUrl))
                throw new Exception("No PDF file provided: upload a file or specify a URL");

            try
            {
                if (pdfUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
                {
                    string localPath = new Uri(pdfUrl).LocalPath;
                    return await System.IO.File.ReadAllBytesAsync(localPath);
                }

                using var httpClient = new System.Net.Http.HttpClient();
                return await httpClient.GetByteArrayAsync(pdfUrl);
            }
            catch (Exception ex)
            {
                throw new Exception("Could not download the PDF file from URL", ex);
            }
        }

        private Merge_PDF_Documents_ViewModel SetViewModel()
        {
            var model = new Merge_PDF_Documents_ViewModel();

            HttpRequest request = ControllerContext.HttpContext.Request;
            UriBuilder uriBuilder = new UriBuilder
            {
                Scheme = request.Scheme,
                Host = request.Host.Host,
                Path = request.PathBase.ToString() + request.Path.ToString(),
                Query = request.QueryString.ToString()
            };
            if (request.Host.Port != null)
                uriBuilder.Port = (int)request.Host.Port;

            string currentPageUrl = uriBuilder.Uri.AbsoluteUri;
            string rootUrl = currentPageUrl.Substring(0, currentPageUrl.Length - "Merge_PDF_Documents".Length);

            model.FirstPdfFileUrl = rootUrl + "/DemoAppFiles/Input/PDF_Files/Multi_Page_Document.pdf";
            model.SecondPdfFileUrl = rootUrl + "/DemoAppFiles/Input/PDF_Files/PDF_Form.pdf";

            return model;
        }

        private string GetDemoFilesPath() => m_hostingEnvironment.ContentRootPath + "/wwwroot" + "/DemoAppFiles/Input/";
    }
}

See Also