Monday, 24 April 2017

Printing PDFs on network printer through c# from application hosted in IIS

Printing on network printers or even in local printers programmatically is always a challenge because of the network printer accessibility through code.

I had a requirement where I need to print pdf on network printer programmatically. I found couple of major issues which is a common issue faced by many people but never got satisfactory or final solution over the internet search. It took couple of days to solve the issues and get the final working solution.

Here are the issues which I faced:
Issue #1: Loading the pdf and sending it to the printer.
During the work I got an article which is well explained about the problem of reading pdf and it has couple of alternative solutions provided but this wasn't a satisfactory solution as our client has do the another software installation as pdf reader.
http://www.bincsoft.com/blog/background-printing-of-pdf-documents/

Here I found PdfiumViewer which is an open source provided under Apache licence which helps to read/modify the pdfs programmatically and client/server doesn't need to have anything new installation.

And here is the code to read the pdf and send it to the printer:


public void SendPdfToPrinter(string filePath, string fileName, string printerNetworkPath)
        {
            // Create the printer settings for our printer
            var printerSettings = new System.Drawing.Printing.PrinterSettings
            {
                PrinterName = printerNetworkPath,
                PrintFileName = fileName,
                PrintRange = System.Drawing.Printing.PrintRange.AllPages,
            };
            printerSettings.DefaultPageSettings.Margins = new System.Drawing.Printing.Margins(0, 0, 0, 0);

            // Now print the PDF document
            using (var document = PdfiumViewer.PdfDocument.Load(filePath))
            {
                using (var printDocument = document.CreatePrintDocument())
                {
                    printDocument.DocumentName = fileName;
                    printDocument.PrinterSettings = printerSettings;
                    printDocument.PrintController = new System.Drawing.Printing.StandardPrintController();
                    printDocument.Print();
                }
            }
        }




Here PdfiumViewer.PdfDocument.Load() provides overloaded method to create a pdfdocument from Stream as well.

Issue #2: Printer accessibility issue.
if your application's application pool identity uses any of the built-in accounts (i.e. LocalSystem, LocalService etc) then you will face a issue of accessing the printer. Because your printers doesn't support these users.
There are two possible solutions to overcome this problem:
  • Set Application Pool identity to a custom user (domain user) which has accessibility to the printers.
  • Use impersonation to impersonate a user and print. below is the code to achieve the impersonation.

    [PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
    public class Impersonation : IDisposable
    {
        private readonly SafeTokenHandle _handle;
        private readonly WindowsImpersonationContext _context;
        bool disposed = false;

        const int LOGON32_PROVIDER_DEFAULT = 0;
        const int LOGON32_LOGON_INTERACTIVE = 2;

        public Impersonation(ImpersonateUserDetails user) : this(user.Domain, user.UserName, user.Password)
        { }
        public Impersonation(string domain, string username, string password)
        {
            var ok = LogonUser(username, domain, password,
                           LOGON32_LOGON_INTERACTIVE, 0, out this._handle);
            if (!ok)
            {
                var errorCode = Marshal.GetLastWin32Error();
                throw new ApplicationException(string.Format("Could not impersonate the elevated user.  LogonUser returned error code {0}.", errorCode));
            }

            this._context = WindowsIdentity.Impersonate(this._handle.DangerousGetHandle());
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposed)
                return;

            if (disposing)
            {
                this._context.Dispose();
                this._handle.Dispose();
            }           
            disposed = true;
        }

        ~Impersonation()
        {
            Dispose(false);
        }


        [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
        private static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);

        sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            private SafeTokenHandle()
                : base(true) { }

            [DllImport("kernel32.dll")]
            [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
            [SuppressUnmanagedCodeSecurity]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool CloseHandle(IntPtr handle);

            protected override bool ReleaseHandle()
            {
                return CloseHandle(handle);
            }
        }
    }

    public class ImpersonateUserDetails
    {
        public string UserName { get; set; }

        public string Password { get; set; }

        public string Domain { get; set; }
    }

One big catch here is that, you must have to use "LOGON32_LOGON_INTERACTIVE" as a "dwLogonType" param value while calling "LogonUser()" method. if you use something else (like LOGON32_LOGON_NEW_CREDENTIALS = 9) you will not get any error but printing won't work. Code will send the document to print queue with owner name as your machine name, file name as printdocument and size as 0 bytes. Overall impersonation is unsuccessful hence printing will fail.




using (new Impersonation("domain", "username", "password"))
{
    string filePath = @"c:\temp\printfile.pdf";
    string fileName = "printfile.pdf";
    \\this should be the fully qualified name of the printer.
    string printerName = @"\\server_name\printer_name"; 
    SendPdfToPrinter(content, docName, printerFullName);
}

Issue #3: Impersonation issue for 32 bit application.
In case if your application is hosted as 32 bit application then you will receive an Win32Exception saying “The handle is not valid." for impersonating the user.This is a well known issue of Microsoft for impersonation and they have patches releases for that as well. For more information refer this:
Printing successfully using impersonation from a 32-bit application-on-a-64-bit-system/

And if your application is using 64-bit application setup then no problem, you are ready to go.