Scenario

A web app that allow user to click on a link/button, then trigger the selected printer and print out the document directly.

Problem & Solution

Due to security reason, there is no way to trigger hardware via JavaScript. Thus, I create a Windows Form Application (WFA) using C#.

This application is actually just a wrapper that wrap a browser (I’m using CefSharp browser), so that I can get the printer list by using C#, then pass to the browser.

1. Create a Windows Form Application

I assume you know how to do this. I’m using Visual Studio in this case.

Also, you need to add a few references that required for this project:

  • System.configuration
  • System.Management
  • System.Web
  • System.Net.Http

2. Download CefSharp browser

You can install CefSharp via Nuget

3. Embed into your main form

Foo.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using CefSharp;
using CefSharp.WinForms;

namespace FooSpace
{
public partial class Foo : Form
{
...
private static string baseUrl = "http://yoursite.com";

// session/cookie property
public static string sessionCookieKey = "PHPSESSID";
private string sessionCookie = null;

public ChromiumWebBrowser browser;

public Foo()
{
InitializeComponent();
InitBrowser();
}

public void InitBrowser()
{
Cef.Initialize(new CefSettings());
browser = new ChromiumWebBrowser(Foo.baseUrl);
this.Controls.Add(browser);
browser.Dock = DockStyle.Fill;
browser.FrameLoadStart += OnFrameLoadStart;
browser.FrameLoadEnd += OnFrameLoadEnd;
browser.DownloadHandler = new FooDownloadHandler();
}

// Chromium delegate
private void OnFrameLoadStart(object sender, FrameLoadStartEventArgs e)
{
// retrieve session cookie
var visitor = new CookieVisitor(all_cookies => {
var sb = new StringBuilder();
foreach (var nameValue in all_cookies)
{
// grab the session cookie
if (nameValue.Item1 == Foo.sessionCookieKey)
{
this.sessionCookie = nameValue.Item2;
// pass the session cookie to download handler
((FooDownloadHandler)browser.DownloadHandler).setSessionCookie(this.sessionCookie);
break;
}
}
});
Cef.GetGlobalCookieManager().VisitAllCookies(visitor);
}
}
}

4. Inject printer list into browser

Edit the same file Foo.cs, and add the following

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// after the page loaded, check if the url is setup printers page
private void OnFrameLoadEnd(object sender, FrameLoadEndEventArgs e)
{
if (e.Url.Contains("setup/printers")) addPrinterOptionsToDropdown();
}

// Javascript
private void addPrinterOptionsToDropdown()
{
// (Optional) Filter a few printer model
Regex regex = new Regex(@"\b(EPSON|Canon)\b", RegexOptions.IgnoreCase);

string js = "$('select').empty();";

foreach (string sPrinters in System.Drawing.Printing.PrinterSettings.InstalledPrinters)
{
MatchCollection matches = regex.Matches(sPrinters);
if (matches.Count < 1) continue;

js += "\n$('select').append($('<option>').val('" + sPrinters + "').text('" + sPrinters + "'));";
}

js += "\n$('select').each(function(i) {"
+ "\n $(this).val($(this).data('value'));"
+ "\n});";

browser.ExecuteScriptAsync(js);
}

5. Create a download handler class

FooDownloadHandler.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using System.Drawing.Printing;
using System.IO;

...

class FooDownloadHandler : IDownloadHandler
{
private string sessionCookie = null;

private string suggestedFilename = null;

// without the session, the file cannot be downloaded
public void setSessionCookie(string sessionCookie)
{
this.sessionCookie = sessionCookie;
}

public void OnBeforeDownload(IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback)
{
if (!callback.IsDisposed)
{
using (callback)
{
callback.Continue(downloadItem.SuggestedFileName, showDialog: true);
}
}
}

// Webclient delegate
private void client_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
// download in progress
}

private void client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
// download completed
}
}

Now if you try to download, it will prompt a “Save as” windows and you can choose where you want to save.

6. Install 3rd-party pdf reader

I’m using Foxit here, download it and install in the Windows machine (client).

7. Pass the downloaded document to selected printer

Edit the file FooDownloadHandler.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// create printer name property
private string printerName = null;

// add handler
public void OnBeforeDownload(IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback)
{
Directory.CreateDirectory(Path.GetTempPath());
this.suggestedFilename = Path.Combine(Path.GetTempPath(), downloadItem.SuggestedFileName);

// the printer name is passed from the web app thru query string
string querystring = downloadItem.Url.Substring(downloadItem.Url.IndexOf('?'));
System.Collections.Specialized.NameValueCollection queryDictionary = System.Web.HttpUtility.ParseQueryString(querystring);
if (queryDictionary.Get("printer") != null && queryDictionary.Get("printer") != "")
{
this.printerName = queryDictionary.Get("printer");
}
else if (!callback.IsDisposed) // without printer name, will prompt "Save as" window
{
using (callback)
{
callback.Continue(downloadItem.SuggestedFileName, showDialog: true);
}
return;
}

// use WebClient to download
WebClient client = new WebClient();
client.Headers.Add(HttpRequestHeader.Cookie, Foo.sessionCookieKey + "=" + this.sessionCookie);
client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(client_DownloadProgressChanged);
client.DownloadFileCompleted += new AsyncCompletedEventHandler(client_DownloadFileCompleted);
client.DownloadFileAsync(new Uri(downloadItem.Url), this.suggestedFilename);
}

private void client_DownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
{
printPDF(this.suggestedFilename, this.printerName);
}

private void printPDF(string filename, string printerName)
{
try
{
string exeFile = @"""C:\Program Files (x86)\Foxit Software\Foxit Reader\FoxitReader.exe""";
ProcessStartInfo processInfo = new ProcessStartInfo();
processInfo.FileName = "\"" + exeFile + "\"";
processInfo.Arguments = "/t \"" + filename + "\" \"" + printerName + "\"";
processInfo.CreateNoWindow = true;
processInfo.UseShellExecute = false;
processInfo.RedirectStandardError = true;
processInfo.RedirectStandardOutput = true;

var process = Process.Start(processInfo);

process.OutputDataReceived += (object sender, DataReceivedEventArgs e) =>
Console.WriteLine("output>>" + e.Data);
process.BeginOutputReadLine();

process.ErrorDataReceived += (object sender, DataReceivedEventArgs e) =>
Console.WriteLine("error>>" + e.Data);
process.BeginErrorReadLine();

process.WaitForExit();
process.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
// after print, delete the temporary file
File.Delete(filename);
}
}

Enjoy it!