Print PDF in .NET – Part 2

In the first part, we have completed the basic PDF printing application. The “Hello World” sample is good as a proof of concept, but it is too simple. You will probably want to have following features in a real application:

  1. Print existing PDF documents.
  2. Print multiple pages.
  3. Setup paper size and orientation.
  4. Preview print output.
  5. Reuse the printing code.

I will incrementally modify the demo application in order to show how to support all of these features.

1. Printing of existing PDF documents

To implement the first feature, slightly modify printButton_Click and printDocument_BeginPrint handlers:

private void printButton_Click(object sender, EventArgs e)
{
	using (OpenFileDialog dlg = new OpenFileDialog())
	{
		dlg.Filter = "PDF files (*.pdf)|*.pdf";

		if (dlg.ShowDialog() == DialogResult.OK)
		{
			m_pdf = new PdfDocument(dlg.FileName);
			m_printDocument.Print();
		}
	}
}

private void printDocument_BeginPrint(object sender, PrintEventArgs e)
{
}

Now the application can print existing documents. However, it prints the first page only. The next step will show how to fix this.

2. Printing of multiple pages

This feature is very simple to implement, too. Here are the changes:

public partial class MainForm : Form
{
	...
	private int m_pageIndex;

	private void printDocument_BeginPrint(object sender, PrintEventArgs e)
	{
		m_pageIndex = 0;
	}

	private void printDocument_PrintPage(object sender, PrintPageEventArgs e)
	{
		m_pdf.Pages[m_pageIndex].Draw(e.Graphics);

		++m_pageIndex;
		e.HasMorePages = (m_pageIndex < m_pdf.PageCount);
	}
}

In the code above, I introduced the variable for the current page index. The printDocument_PrintPage method increments the variable and compares its value with the total page count. The method sets the PrintPageEventArgs.HasMorePages property to true if there are non-printed pages left.

3. Print settings

I recommend you to use the PrintDialog class from the .NET Framework for the feature. Add new method to the MainForm class:

private void showPrintDialog()
{
	using (PrintDialog printDialog = new PrintDialog())
	{
		printDialog.AllowSomePages = true;
		printDialog.AllowCurrentPage = true;
		printDialog.AllowSelection = true;

		printDialog.PrinterSettings.MinimumPage = 1;
		printDialog.PrinterSettings.MaximumPage = m_pdf.PageCount;
		printDialog.PrinterSettings.FromPage = printDialog.PrinterSettings.MinimumPage;
		printDialog.PrinterSettings.ToPage = printDialog.PrinterSettings.MaximumPage;

		if (printDialog.ShowDialog() == DialogResult.OK)
		{
			using (PrintDocument printDocument = new PrintDocument())
			{
				printDocument.BeginPrint += printDocument_BeginPrint;
				printDocument.PrintPage += printDocument_PrintPage;
				printDocument.EndPrint += printDocument_EndPrint;

				printDocument.PrinterSettings = printDialog.PrinterSettings;
				printDocument.Print();
			}
		}
	}
}

The method creates an instance of the PrintDialog class and configures it. The code defines what options to enable in the Print range block of the dialog and sets default print range up. There are some other properties provide by the PrintDialog class. You might want to check them out too.

With the showPrintDialog method there is no need for m_printDocument field and related code. Therefore, remove the code that deals with m_printDocument field from the constructor of MainForm class and its Dispose method.

In printButton_Click handler, replace the following code:

m_pdf = new PdfDocument(dlg.FileName);
m_printDocument.Print();

with the new code

using (m_pdf = new PdfDocument(dlg.FileName))
    showPrintDialog();

Remove m_pdf.Dispose(); call from printDocument_EndPrint method:

private void printDocument_EndPrint(object sender, PrintEventArgs e)
{
}

Build and run the application. Click the Print button and select a PDF file. You should see a dialog like the following:

PDF print dialog

Now, the application shows PrintDialog before printing, but the code does not use selected print settings.
Add new m_lastPageIndex field to the MainForm class and modify its printDocument_BeginPrint method:

private int m_lastPageIndex;
...

private void printDocument_BeginPrint(object sender, PrintEventArgs e)
{
	PrintDocument printDocument = (PrintDocument)sender;
	switch (printDocument.PrinterSettings.PrintRange)
	{
		case PrintRange.Selection:
		case PrintRange.CurrentPage:
			{
				m_pageIndex = 0;
				m_lastPageIndex = 0;
				break;
			}

		case PrintRange.SomePages:
			{
				m_pageIndex = Math.Max(0, printDocument.PrinterSettings.FromPage - 1);
				m_lastPageIndex = Math.Min(m_pdf.PageCount - 1, printDocument.PrinterSettings.ToPage - 1);
				break;
			}

		case PrintRange.AllPages:
		default:
			{
				m_pageIndex = 0;
				m_lastPageIndex = m_pdf.PageCount - 1;
				break;
			}
	}
}

The code above sets the first page as the print range if user picks Selection range in the print dialog.
Please note that print dialog uses 1-based indexes:

printDialog.PrinterSettings.MinimumPage = 1;
printDialog.PrinterSettings.MaximumPage = m_pdf.PageCount;

Thus, the code subtracts 1 from the minimum and maximum range indexes to get PDF page indexes, which are 0-based.
Do not forget to modify the condition for the e.HasMorePages property in the printDocument_PrintPage method. The condition should use the m_lastPageIndex field:

e.HasMorePages = (m_pageIndex <= m_lastPageIndex);

With this feature complete, the application can show print dialog and print PDF documents based on the choices made by its user.

4. Print preview

.NET Framework provides PrintPreviewDialog class that I will use for the feature. The PrintPreviewDialog class uses PrintDocument class, so there are just a few changes needed.

Add new method to the MainForm class:

private void showPrintPreview()
{
	using (PrintPreviewDialog previewDialog = new PrintPreviewDialog())
	{
		using (PrintDocument printDocument = new PrintDocument())
		{
			printDocument.BeginPrint += printDocument_BeginPrint;
			printDocument.PrintPage += printDocument_PrintPage;
			printDocument.EndPrint += printDocument_EndPrint;

			previewDialog.Document = printDocument;
			previewDialog.ShowDialog();
		}
	}
}

Add new Preview button to the main form and use the following code as the click handler for the button:

private void previewButton_Click(object sender, EventArgs e)
{
	using (OpenFileDialog dlg = new OpenFileDialog())
	{
		dlg.Filter = "PDF files (*.pdf)|*.pdf";

		if (dlg.ShowDialog() == DialogResult.OK)
		{
			using (m_pdf = new PdfDocument(dlg.FileName))
				showPrintPreview();
		}
	}
}

As you can see, there is some very similar code in printButton_Click and previewButton_Click methods. I will address that on the next step. For now, run the app and click the Preview button. The application should show a dialog like the following:

PDF print preview

Please note that PrintPreviewDialog class has some drawbacks:

  • It renders the entire document before the preview appears. This is annoying for long documents.
  • It does not provide means for choosing the printer, adjusting the page layout, or selecting specific pages to print.
  • It does not support scrolling with a mouse wheel and navigation with keyboard.
  • It does not allow customization to its look.
  • It creates and maintains cache for page images. Therefore, the class limits the size of the documents that can be previewed.

You may develop a custom print preview dialog yourselves or use one of existing 3-rd party implementations, like https://www.codeproject.com/Articles/38758/An-Enhanced-PrintPreviewDialog.

5. Refactoring

There is some technical debt in the printing code. There are two problems:

  • All printing-related code is in the MainForm class. Other parts of the application cannot reuse it.
  • Print and print preview dialogs share come common code.

It is better to create new PdfPrintDocument class and move all printing-related code from the MainForm class to the new class. Internally, the new class will use PrintDocument class and its events.

class PdfPrintDocument : IDisposable
{
	private PrintDocument m_printDocument;
	private PdfDocument m_pdf;

	private int m_pageIndex;
	private int m_lastPageIndex;

	public PdfPrintDocument(PdfDocument pdf)
	{
		if (pdf == null)
			throw new ArgumentNullException("pdf");

		m_pdf = pdf;

		m_printDocument = new PrintDocument();
		m_printDocument.BeginPrint += printDocument_BeginPrint;
		m_printDocument.PrintPage += printDocument_PrintPage;
		m_printDocument.EndPrint += printDocument_EndPrint;
	}

	public PrintDocument PrintDocument
	{
		get { return m_printDocument; }
	}

	public void Dispose()
	{
		m_printDocument.Dispose();
	}

	public void Print(PrinterSettings settings)
	{
		if (settings == null)
			throw new ArgumentNullException("settings");

		m_printDocument.PrinterSettings = settings;
		m_printDocument.Print();
	}

	private void printDocument_BeginPrint(object sender, PrintEventArgs e)
	{
		PrintDocument printDocument = (PrintDocument)sender;
		switch (printDocument.PrinterSettings.PrintRange)
		{
			case PrintRange.Selection:
			case PrintRange.CurrentPage:
				{
					m_pageIndex = 0;
					m_lastPageIndex = 0;
					break;
				}

			case PrintRange.SomePages:
				{
					m_pageIndex = Math.Max(0, printDocument.PrinterSettings.FromPage - 1);
					m_lastPageIndex = Math.Min(m_pdf.PageCount - 1, printDocument.PrinterSettings.ToPage - 1);
					break;
				}

			case PrintRange.AllPages:
			default:
				{
					m_pageIndex = 0;
					m_lastPageIndex = m_pdf.PageCount - 1;
					break;
				}
		}
	}

	private void printDocument_PrintPage(object sender, PrintPageEventArgs e)
	{
		m_pdf.Pages[m_pageIndex].Draw(e.Graphics);

		++m_pageIndex;
		e.HasMorePages = (m_pageIndex <= m_lastPageIndex);
	}

	private void printDocument_EndPrint(object sender, PrintEventArgs e)
	{
	}
}

With the PdfPrintDocument class, the code of the MainForm class is simpler:

public partial class MainForm : Form
{
	public MainForm()
	{
		InitializeComponent();
	}

	private void showPrintDialog(PdfDocument pdf)
	{
		using (PrintDialog printDialog = new PrintDialog())
		{
			printDialog.AllowSomePages = true;
			printDialog.AllowCurrentPage = true;
			printDialog.AllowSelection = true;

			printDialog.PrinterSettings.MinimumPage = 1;
			printDialog.PrinterSettings.MaximumPage = pdf.PageCount;
			printDialog.PrinterSettings.FromPage = printDialog.PrinterSettings.MinimumPage;
			printDialog.PrinterSettings.ToPage = printDialog.PrinterSettings.MaximumPage;

			if (printDialog.ShowDialog() == DialogResult.OK)
			{
				using (PdfPrintDocument printDocument = new PdfPrintDocument(pdf))
					printDocument.Print(printDialog.PrinterSettings);
			}
		}
	}

	private void showPrintPreview(PdfDocument pdf)
	{
		using (PrintPreviewDialog previewDialog = new PrintPreviewDialog())
		{
			using (PdfPrintDocument printDocument = new PdfPrintDocument(pdf))
			{
				previewDialog.Document = printDocument.PrintDocument;
				previewDialog.ShowDialog();
			}
		}
	}

	private void printButton_Click(object sender, EventArgs e)
	{
		using (OpenFileDialog dlg = new OpenFileDialog())
		{
			dlg.Filter = "PDF files (*.pdf)|*.pdf";

			if (dlg.ShowDialog() == DialogResult.OK)
			{
				using (PdfDocument pdf = new PdfDocument(dlg.FileName))
					showPrintDialog(pdf);
			}
		}
	}

	private void previewButton_Click(object sender, EventArgs e)
	{
		using (OpenFileDialog dlg = new OpenFileDialog())
		{
			dlg.Filter = "PDF files (*.pdf)|*.pdf";

			if (dlg.ShowDialog() == DialogResult.OK)
			{
				using (PdfDocument pdf = new PdfDocument(dlg.FileName))
					showPrintPreview(pdf);
			}
		}
	}
}

Like before, create new PdfPrintHelper class and move the showPrintDialog and showPrintPreview methods to it:

static class PdfPrintHelper
{
	public static void ShowPrintDialog(PdfDocument pdf)
	{
		using (PrintDialog printDialog = new PrintDialog())
		{
			printDialog.AllowSomePages = true;
			printDialog.AllowCurrentPage = true;
			printDialog.AllowSelection = true;

			printDialog.PrinterSettings.MinimumPage = 1;
			printDialog.PrinterSettings.MaximumPage = pdf.PageCount;
			printDialog.PrinterSettings.FromPage = printDialog.PrinterSettings.MinimumPage;
			printDialog.PrinterSettings.ToPage = printDialog.PrinterSettings.MaximumPage;

			if (printDialog.ShowDialog() == DialogResult.OK)
			{
				using (PdfPrintDocument printDocument = new PdfPrintDocument(pdf))
					printDocument.Print(printDialog.PrinterSettings);
			}
		}
	}

	public static void ShowPrintPreview(PdfDocument pdf)
	{
		using (PrintPreviewDialog previewDialog = new PrintPreviewDialog())
		{
			using (PdfPrintDocument printDocument = new PdfPrintDocument(pdf))
			{
				previewDialog.Document = printDocument.PrintDocument;
				previewDialog.ShowDialog();
			}
		}
	}
}

The last refactoring step is to remove duplication from the printButton_Click and previewButton_Click methods:

public partial class MainForm : Form
{
	public MainForm()
	{
		InitializeComponent();
	}

	private void printButton_Click(object sender, EventArgs e)
	{
		processExistingPdfDocument(PdfPrintHelper.ShowPrintDialog);
	}

	private void previewButton_Click(object sender, EventArgs e)
	{
		processExistingPdfDocument(PdfPrintHelper.ShowPrintPreview);
	}

	private void processExistingPdfDocument(Action action)
	{
		using (OpenFileDialog dlg = new OpenFileDialog())
		{
			dlg.Filter = "PDF files (*.pdf)|*.pdf";

			if (dlg.ShowDialog() == DialogResult.OK)
			{
				using (PdfDocument pdf = new PdfDocument(dlg.FileName))
					action(pdf);
			}
		}
	}
}

6. Tuning

The application is almost complete. The only feature that is missing is the support for PDF documents with different page sizes and rotated pages.

The PrintDocument fires QueryPageSettings event immediately before each PrintPage event. The handler for the QueryPageSettings event is the ideal place to setup page size and orientation.

Add such a handler to the PdfPrintDocument class:

public PdfPrintDocument(PdfDocument pdf)
{
...
	m_printDocument.QueryPageSettings += printDocument_QueryPageSetting;
}
...

private void printDocument_QueryPageSetting(object sender, QueryPageSettingsEventArgs e)
{
	PdfPage page = m_pdf.Pages[m_pageIndex];

	// PaperSize constructor accepts arguments in hundredth of inch
	double scale = (double)(100 / page.Canvas.Resolution);
	PaperSize paperSize = new PaperSize("Custom", (int)(page.Width * scale), (int)(page.Height * scale));
	e.PageSettings.PaperSize = paperSize;

	bool rotated = (page.Rotation == PdfRotation.Rotate270 || page.Rotation == PdfRotation.Rotate90);
	e.PageSettings.Landscape = rotated;
}

Thanks to the printDocument_QueryPageSetting method, rotated PDF pages will be properly oriented on the sheet of paper.

Summary

The PDF printing application is complete. It can open and print existing PDF documents. The application shows print settings dialog before printing, where user can select the printer, the page range and the number of copies. The application can also show the print preview dialog.

There are still number of possible improvements to the application. Here is the list of some useful features that you might need to have in a real application:

  • Support for password-protected PDF documents.
  • Printing of multiple pages on one sheet of paper.
  • Printing of PDF pages based on the page size selected in the print dialog.
  • Improvements to the print preview dialog described in the Print preview section above.

Implementation of any of these features is the homework for you 🙂 I would be glad to help you – just ask in the comments, or write me a letter.

Download the full source code of the PDF printing application.