Hi,
I've got to start printing PDFs - some will come from Microsoft Word, some will be from EO.PDF's HtmlToPdf feature, and some will come from elsewhere (photocopiers, etc).
I happened to notice in the EO.PDF 2017 release there's a new Print() method on PdfDocument. This is perfect.
However... There's no way to know when the print has finished. I know things are queued in Windows print queues - I literally mean that I want to get one of your `ManualTask` instances, or something like it, to know when the print job has been rendered (or perhaps thrown an error) so that I can do things like release memory, close my process, report back to the user, etc.
To get a better understanding of printing I let ReSharper show me some of the code behind the Print() method. From what I can see it looks like that Print() method creates a new PdfViewer, slaps it into a WinForms form, loads the PDF into it and then does the print.
So I wrote my own version of that code - it's pretty much identical.
The only difference is that I added some reflection to get to the underlying WebView of the PdfViewer (which itself is just a WebViewHost) and then added a listener to the AfterPrint event.
However I'm finding that AfterPrint event is not getting called. This is despite the print occurring succesfully.
In case I'm making this much harder than required, I want to print PDFs in two circumstances
1. Create a simple console app that accepts a printer name, a printer bin and then a list of PDFs on the command line. It'll load each PDF into a byte array and print to that printer. The console app will run unattended. I'll need to at least wait until the generation of the print job is complete before letting it terminate.
2. Similar to #1, but it might be a longer living app, running as a Windows service. It'll need to report back to a web service when it has succesfully generated a print job or if some error happened. So again I'll need to at least know if printing was successful.
After reading the code I'm also wary of knowing whether or not there could be memory leaks. A new WinForms form is created to host the PdfViewer control and a listener is added to PdfViewer's IsReadyChanged. It may well be the case that this all gets garbage collected but with so many moving parts, threads, etc I just want to be sure. There's no Close() or Dispose() method on PdfDocument to get rid of the ThreadRunner instance for example. That may well be fine - again I'm just wanting to be sure that the ThreadRunner doesn't register itself in some static list. For example, I was previously bitten by not calling HtmlToPdf.ClearResult() in production and ended up with several GB of RAM used in my small website - I wasn't using the static method variant of doing that conversion so it never occurred to me to need to clear some statically held per-thread state.
I'm happy with a workaround or to be shown some setting that I've missed. It may well be that I need to do my own WebView instance, load a PDF into it, ensure that's finished (see
https://www.essentialobjects.com/forum/postst9675_EOWebBrowser--Detect-PDF-Has-Finished-Loading.aspx), hook the print event and then be ready to go.
Thank you very much for your help!
Relevant code below - some bits like the Arguments class haven't been included since it just indicates duplex, printer name, etc settings. The Log calls are just to Serilog and can be commented out.
Code: C#
public static class PdfPrint
{
public static Task Print(Arguments settings)
{
var result = new TaskCompletionSource<object>();
try
{
//configuration of printer settings
var printerSettings = new PrinterSettings()
{
Copies = (short) settings.Copies,
Duplex = settings.Duplex.ToSystemDrawing(settings.Orientation == Orientation.Landscape),
PrinterName = settings.DestinationPrinterName
};
//and page settings
var pageSettings = new PageSettings()
{
Color = settings.Colour,
Landscape = settings.Orientation == Orientation.Landscape,
PaperSource = settings.ToPaperSource(),
//PrinterSettings = printerSettings
};
Log.Information("Printer and page settings {@printerSettings} {@pageSettings}", printerSettings, pageSettings);
//grab the PDF contents
var bytes = File.ReadAllBytes(settings.SourceFileName);
Log.Information("PDF bytes loaded {bytes}", bytes.Length);
//begin the tedious task of handling EO.PDF rendering & printing
var manualTask = new ManualTask();
ThreadRunner threadRunner = null;
DoxPdfViewer pdfViewer = null;
//Monitor code not strictly necessary but doesn't hurt
lock (manualTask)
{
if (threadRunner == null)
{
threadRunner = new ThreadRunner();
}
Log.Information("EO.PDF ThreadRunner created");
threadRunner.Send(() =>
{
pdfViewer = new DoxPdfViewer();
pdfViewer.IsReadyChanged += (sender, args) =>
{
if (pdfViewer.IsReady)
{
Log.Information("EO.PDF PdfViewer is ready");
manualTask.Complete();
}
else
{
Log.Warning("EO.PDF PdfViewer ReadyChanged fired but still not ready");
}
};
pdfViewer.Dock = DockStyle.Fill;
var form = new Form()
{
ShowInTaskbar = false,
FormBorderStyle = FormBorderStyle.None,
WindowState = FormWindowState.Minimized,
Location = new Point(-10000, -10000),
Size = new Size(600, 400)
};
form.Controls.Add(pdfViewer);
form.Show();
});
}
if (!manualTask.WaitOne(5000))
{
Log.Error("EO.PDF PdfViewer initialisation took too long");
throw new Exception("Timed out waiting for pdf viewer to initialise");
}
Log.Debug("EO.PDF PdfViewer initialisation complete - moving on to print phase");
threadRunner.Post(() =>
{
Log.Information("EO.PDF PdfViewer commencing PDF load");
var loadWait = pdfViewer.Load(bytes);
loadWait.OnDone(() =>
{
Log.Information("EO.PDF PdfViewer PDF load completed - proceeding with print");
var webView = pdfViewer.WebView;
webView.AfterPrint += (sender, afterPrintEventArgs) =>
{
Log.Information("EO.PDF PdfViewer WebView indicated print call has completed with status: {afterPrint}", afterPrintEventArgs.Result);
if (afterPrintEventArgs.Result == PrintResult.OK)
{
result.SetResult(null);
}
else
{
result.SetException(new Exception("EO.PDF PdfViewer WebView indicated print failure " + afterPrintEventArgs.Result.ToString()));
}
};
//might this throw an error itself - I could add a try/catch here to properly propogate an exception into my TaskCompletionSource
pdfViewer.Print(printerSettings, pageSettings);
Log.Debug("EO.PDF PdfViewer print command issued");
});
});
}
catch (Exception e)
{
result.SetException(e);
}
return result.Task;
}
}
public class DoxPdfViewer:PdfViewer
{
private static Lazy<MethodInfo> webViewHostPropertyGetter;
static DoxPdfViewer()
{
webViewHostPropertyGetter = new Lazy<MethodInfo>(() => {
var type = typeof(WebViewHost);
var method = type
.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic)
.Where(p => p.PropertyType == typeof(WebView))
.First()
.GetGetMethod(true);
return method;
});
}
public DoxPdfViewer()
{
this.webView = new Lazy<WebView>(() =>
{
var getter = webViewHostPropertyGetter.Value;
//grab its value
var webViewFieldValue = (WebView)getter.Invoke(this, new object[0]);
return webViewFieldValue;
});
}
private readonly Lazy<WebView> webView;
public WebView WebView => this.webView.Value;
}