How to merge PDF files in C#
Version scope: This example uses iTextSharp (iText 5), a legacy library. Its APIs differ from current iText releases. For new projects, review the official iText version and support guidance and the applicable licensing before choosing a package.
To merge two or more PDF files into one using iTextSharp in C#, you create a
Document, attach a
PdfCopy writer to an output file, open
the document, and copy every page of every input PDF into it. In this
article you'll build a reusable
MergeMultiplePDF method that does exactly
that. Then you'll use it in a real scenario: exporting several Crystal
Reports to PDF and combining them into a single file.
By the end, you'll know what iTextSharp is, how to add it to a project, how the merge method works line by line, and what to check when it fails.
The problem this solves
Reporting features often export to several formats: PDF, DOC, XLSX, CSV, RPT, and more. Sometimes a client wants more than one export, and they want all of it as a single document.
Suppose you export reports from "Crystal Report Viewer" to PDF, and you end up with a pile of separate files. The client wants to print all of them with one click, which means merging them into one PDF first.
I ran into this requirement recently. I posted it on the C# Corner forum and searched many websites, but nothing fit. A senior colleague suggested a simple approach: loop over the data, export each report to a PDF in a temporary folder, merge those files with iTextSharp, and delete the temporary files. That approach worked, and it's what this article walks through.
What is iTextSharp?
iTextSharp is the .NET port of the iText 5 library. It lets you create, read, and modify PDF files from C# code, including converting simple HTML or page output into PDF and merging existing PDFs.
Two things to know before you use it:
- iTextSharp 5.x is licensed under the AGPL. If you use it in a closed-source commercial application, read the license terms or buy a commercial license from iText.
-
It's the older generation of the library. Newer projects usually use
iText 7 or later, which has a different API (for example,
PdfMerger). The code in this article targets iTextSharp 5.x.
Requirements
The solution needs to do three things:
- Export multiple Crystal Reports to PDF with one click and save them in a temporary folder inside the project directory.
- Merge all exported PDFs into a single PDF.
- Remove the temporary exported files and any old merged file left over from a previous run.
Implementation
Prerequisites
- A .NET Framework project in Visual Studio (Crystal Reports for .NET Framework requires it).
-
iTextSharp 5.x added through NuGet. Open the Package Manager Console and
run
Install-Package iTextSharp. Avoid downloadingitextsharp.dllfrom random websites, since you can't verify what's inside. -
For the second example, the Crystal Reports runtime and a
.rptreport file in your project. -
Basic C# knowledge: loops, arrays, and
usingblocks.
Step 1: Create the merge method
Add this method to your code-behind or to a helper class. It takes an array of PDF file paths and the path of the file to create.
public static void MergeMultiplePDF(string[] PDFfileNames, string OutputFile)
{
// Create the document object
iTextSharp.text.Document PDFdoc = new iTextSharp.text.Document();
// The FileStream is disposed automatically at the end of the using block
using (System.IO.FileStream MyFileStream = new System.IO.FileStream(OutputFile, System.IO.FileMode.Create))
{
// PdfCopy writes the imported pages into the output document
iTextSharp.text.pdf.PdfCopy PDFwriter = new iTextSharp.text.pdf.PdfCopy(PDFdoc, MyFileStream);
// Open the document before adding pages
PDFdoc.Open();
foreach (string fileName in PDFfileNames)
{
// Create a reader for one input PDF
iTextSharp.text.pdf.PdfReader PDFreader = new iTextSharp.text.pdf.PdfReader(fileName);
try
{
PDFreader.ConsolidateNamedDestinations();
// Copy every page (page numbers start at 1)
for (int i = 1; i <= PDFreader.NumberOfPages; i++)
{
iTextSharp.text.pdf.PdfImportedPage page = PDFwriter.GetImportedPage(PDFreader, i);
PDFwriter.AddPage(page);
}
// Keep form fields if the input PDF has any
iTextSharp.text.pdf.PRAcroForm form = PDFreader.AcroForm;
if (form != null)
{
PDFwriter.CopyAcroForm(PDFreader);
}
}
finally
{
// Always release the input file, even if copying fails
PDFreader.Close();
}
}
// Closing the document also closes the PdfCopy writer
PDFdoc.Close();
}
}
How the method works
The method has two parameters.
PDFfileNames is a
string array holding the paths of the
input PDFs, and OutputFile is the path of
the merged file.
PdfCopy is the class that does the
merging. It's built to copy pages from existing PDFs, which is why it's
used here instead of PdfWriter. For each
input file, a PdfReader opens the source,
GetImportedPage grabs a page, and
AddPage appends it to the output. The
files are merged in the order they appear in the array.
Expected result: after the method returns,
OutputFile exists and its page count
equals the total pages of all input files.
Step 2: Use the method with exported Crystal Reports
Now let's use the method in a real workflow. The flowchart below shows the order of operations: export one PDF per grid row, merge them, open the result, and clean up.
In short: the program reads a JobId from each grid row and loads its data. If the row has data, it exports a temporary PDF. After the last row, it merges the PDFs, opens the result, and deletes the temporary files.
The code below follows that flow. It assumes a WinForms app with a
DataGridView named
dataGridView1 that has a
JobId column, and the usual
using directives for
System.IO,
System.Data,
System.Diagnostics,
System.Collections.Generic,
CrystalDecisions.CrystalReports.Engine,
and CrystalDecisions.Shared.
List<string> pdfFiles = new List<string>();
string tempFolder = Path.Combine(Application.StartupPath, "Temp");
string mergedFile = Path.Combine(tempFolder, "Codingvila_Articles_Report.pdf");
try
{
// Make sure the temp folder exists
Directory.CreateDirectory(tempFolder);
// Create one PDF per grid row
for (int j = 0; j < dataGridView1.Rows.Count; j++)
{
int jobId = Convert.ToInt32(dataGridView1.Rows[j].Cells["JobId"].Value);
// Return the data for this job
DataTable dtDetail = new My_GlobalClass().GetDataTable(jobId);
if (dtDetail.Rows.Count > 0)
{
ReportDocument cryRpt = new ReportDocument();
try
{
// Load the .rpt file and assign the data source
string rptPath = Path.Combine(Application.StartupPath, "RPT", "rptCodingvila_Articles_Report.rpt");
cryRpt.Load(rptPath);
cryRpt.SetDataSource(dtDetail);
// Export the report to a temp PDF and remember its path
string pdfPath = Path.Combine(tempFolder, "Codingvila_Articles_Report" + jobId + ".pdf");
cryRpt.ExportToDisk(ExportFormatType.PortableDocFormat, pdfPath);
pdfFiles.Add(pdfPath);
}
finally
{
// Release Crystal Reports resources for this report
cryRpt.Close();
cryRpt.Dispose();
}
}
}
if (pdfFiles.Count > 0)
{
// Remove an old merged file if one is left over
if (File.Exists(mergedFile))
{
File.Delete(mergedFile);
}
// Merge all temp PDFs into one file
MergeMultiplePDF(pdfFiles.ToArray(), mergedFile);
// Open the merged PDF
Process.Start(mergedFile);
}
}
catch (Exception ex)
{
XtraMessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// Delete the temp PDFs, even if something above failed
foreach (string item in pdfFiles)
{
if (File.Exists(item))
{
File.Delete(item);
}
}
}
What the code does
The loop reads the JobId value from each grid row. It passes that
ID to GetDataTable, a method in the author's
My_GlobalClass that returns a
DataTable. Replace it with your own
data-access code.
For each row that returns data, the code creates a
ReportDocument, loads
rptCodingvila_Articles_Report.rpt from
the RPT folder, and assigns the data with
SetDataSource.
ExportToDisk then writes a PDF to the
Temp folder, and the path goes into the
pdfFiles list. The report viewer control
isn't needed for exporting, so those lines are gone.
After the loop, if at least one PDF was exported, the code deletes any old
merged file, calls MergeMultiplePDF, and
opens the result. The finally block
removes the temporary PDFs whether or not the merge succeeded, and the
merged file stays because it isn't in the list.
Expected result: the default PDF viewer opens
Codingvila_Articles_Report.pdf, which
contains every report in grid order. The
Temp folder then holds only that file.
Common errors and how to fix them
-
DirectoryNotFoundExceptionwhen exporting: theTempfolder doesn't exist. The code above creates it withDirectory.CreateDirectory. -
IOExceptionsaying the file is in use: the merged PDF is still open in a viewer, so it can't be deleted or overwritten. Close it and run the code again. -
InvalidPdfExceptionwhen creating aPdfReader: one of the input files is empty or corrupted. Check that each export finished and that the file opens in a PDF viewer. -
Every page is the same report: this usually means the loop reads
a fixed row such as
Rows[0]instead ofRows[j].
Limitations to keep in mind
This approach copies pages and form fields. Check that other features your PDFs rely on, such as bookmarks, survive the merge before you ship it. If your project uses a newer iText version, the merge code will differ, because iText 7 and later have a different API.
Summary
To merge multiple PDF files into one in C#, open a
Document, attach a
PdfCopy writer, and add each input page
with GetImportedPage and
AddPage. Key takeaways:
- Install iTextSharp from NuGet and check the AGPL license before using it in commercial software.
-
Use
PdfCopyfor merging, and close eachPdfReaderin afinallyblock. -
When exporting Crystal Reports in a loop, dispose each
ReportDocumentand clean up temp files in afinallyblock. - Index the grid row with the loop variable, and create the temp folder before you export.
