/*
pdf scribe virtual pdf printer
all credits to Sherman Chan (https://github.com/stchan/PdfScribe)
this code is licensed under LGPL v3 (https://www.gnu.org/licenses/lgpl-3.0.en.html)
changes from original code are surrounded by "myrtille" region tags
*/
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using Myrtille.Helpers;
namespace Myrtille.Printer
{
public class Program
{
#region Message constants
const string errorDialogCaption = "PDF Scribe"; // Error taskdialog caption text
const string errorDialogInstructionPDFGeneration = "There was a PDF generation error.";
const string errorDialogInstructionCouldNotWrite = "Could not create the output file.";
const string errorDialogInstructionUnexpectedError = "There was an internal error. Enable tracing for details.";
const string errorDialogTextFileInUse = "{0} is being used by another process.";
const string errorDialogTextGhostScriptConversion = "Ghostscript error code {0}.";
const string warnFileNotDeleted = "{0} could not be deleted.";
#endregion
#region Other constants
const string traceSourceName = "PdfScribe";
const string defaultOutputFilename = "PDFSCRIBE.PDF";
#endregion
static TraceSource logEventSource = new TraceSource(traceSourceName);
#region myrtille
static Process parentProcess = null;
#endregion
[STAThread]
static void Main(string[] args)
{
// Install the global exception handler
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(Application_UnhandledException);
#region myrtille
// enable the line below to debug this postscript redirector; disable otherwise
// CAUTION! if this print job is from FreeRDP windowless (non user interactive), use the sleep delay instead
//MessageBox.Show("Attach the .NET debugger to the 'PDF Debug' Myrtille.Printer.exe process now for debug. Click OK when ready...", "PDF Debug");
//Thread.Sleep(10000);
// retrieve the parent process
parentProcess = ProcessHelper.GetParentProcess();
if (parentProcess == null || parentProcess.ProcessName != "spoolsv")
{
if (Environment.UserInteractive)
{
MessageBox.Show("This program is meant to be called by the spooler service",
errorDialogCaption,
MessageBoxButtons.OK,
MessageBoxIcon.Error,
MessageBoxDefaultButton.Button1,
MessageBoxOptions.DefaultDesktopOnly);
}
return;
}
#endregion
String standardInputFilename = Path.GetTempFileName();
String outputFilename = String.Empty;
try
{
using (BinaryReader standardInputReader = new BinaryReader(Console.OpenStandardInput()))
{
using (FileStream standardInputFile = new FileStream(standardInputFilename, FileMode.Create, FileAccess.ReadWrite))
{
standardInputReader.BaseStream.CopyTo(standardInputFile);
}
}
if (GetPdfOutputFilename(ref outputFilename))
{
// Remove the existing PDF file if present
File.Delete(outputFilename);
// Only set absolute minimum parameters, let the postscript input
// dictate as much as possible
String[] ghostScriptArguments = { "-dBATCH", "-dNOPAUSE", "-dSAFER", "-sDEVICE=pdfwrite",
String.Format("-sOutputFile={0}", outputFilename), standardInputFilename };
#region myrtille
//GhostScript64.CallAPI(ghostScriptArguments);
// the current process will usually run in 32 bits, but that also depends on the OS, spooler, printer drivers, executable location, etc.
// call the ghostscript dll accordingly
if (Environment.Is64BitOperatingSystem && Environment.Is64BitProcess)
{
GhostScript64.CallAPI(ghostScriptArguments);
}
else
{
GhostScript32.CallAPI(ghostScriptArguments);
}
#endregion
}
}
catch (IOException ioEx)
{
// We couldn't delete, or create a file
// because it was in use
logEventSource.TraceEvent(TraceEventType.Error,
(int)TraceEventType.Error,
errorDialogInstructionCouldNotWrite +
Environment.NewLine +
"Exception message: " + ioEx.Message);
DisplayErrorMessage(errorDialogCaption,
errorDialogInstructionCouldNotWrite + Environment.NewLine +
String.Format("{0} is in use.", outputFilename));
}
catch (UnauthorizedAccessException unauthorizedEx)
{
// Couldn't delete a file
// because it was set to readonly
// or couldn't create a file
// because of permissions issues
logEventSource.TraceEvent(TraceEventType.Error,
(int)TraceEventType.Error,
errorDialogInstructionCouldNotWrite +
Environment.NewLine +
"Exception message: " + unauthorizedEx.Message);
DisplayErrorMessage(errorDialogCaption,
errorDialogInstructionCouldNotWrite + Environment.NewLine +
String.Format("Insufficient privileges to either create or delete {0}", outputFilename));
}
catch (ExternalException ghostscriptEx)
{
// Ghostscript error
logEventSource.TraceEvent(TraceEventType.Error,
(int)TraceEventType.Error,
String.Format(errorDialogTextGhostScriptConversion, ghostscriptEx.ErrorCode.ToString()) +
Environment.NewLine +
"Exception message: " + ghostscriptEx.Message);
DisplayErrorMessage(errorDialogCaption,
errorDialogInstructionPDFGeneration + Environment.NewLine +
String.Format(errorDialogTextGhostScriptConversion, ghostscriptEx.ErrorCode.ToString()));
}
finally
{
try
{
File.Delete(standardInputFilename);
}
catch
{
logEventSource.TraceEvent(TraceEventType.Warning,
(int)TraceEventType.Warning,
String.Format(warnFileNotDeleted, standardInputFilename));
}
logEventSource.Flush();
}
}
///
/// All unhandled exceptions will bubble their way up here -
/// a final error dialog will be displayed before the crash and burn
///
///
///
static void Application_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
logEventSource.TraceEvent(TraceEventType.Critical,
(int)TraceEventType.Critical,
((Exception)e.ExceptionObject).Message + Environment.NewLine +
((Exception)e.ExceptionObject).StackTrace);
#region myrtille
// if an exception message is set, use it
var message = ((Exception)e.ExceptionObject).Message;
DisplayErrorMessage(errorDialogCaption,
//errorDialogInstructionUnexpectedError);
!string.IsNullOrEmpty(message) ? message : errorDialogInstructionUnexpectedError);
#endregion
}
static bool GetPdfOutputFilename(ref String outputFile)
{
bool filenameRetrieved = false;
#region myrtille
// is this print job from FreeRDP? (look into the spooler environment)
outputFile = GetFreeRDPOutputFilename(parentProcess);
if (!string.IsNullOrEmpty(outputFile))
{
filenameRetrieved = true;
}
#endregion
// if not a FreeRDP print job, follow the standard pdf scribe code
else
{
// ensure the current process is running in user interactive mode before showing any dialog
//switch (Properties.Settings.Default.AskUserForOutputFilename)
switch (Properties.Settings.Default.AskUserForOutputFilename && Environment.UserInteractive)
{
case (true):
using (SetOutputFilename dialogOwner = new SetOutputFilename())
{
dialogOwner.TopMost = true;
dialogOwner.TopLevel = true;
dialogOwner.Show(); // Form won't actually show - Application.Run() never called
// but having a topmost/toplevel owner lets us bring the SaveFileDialog to the front
dialogOwner.BringToFront();
using (SaveFileDialog pdfFilenameDialog = new SaveFileDialog())
{
pdfFilenameDialog.AddExtension = true;
pdfFilenameDialog.AutoUpgradeEnabled = true;
pdfFilenameDialog.CheckPathExists = true;
pdfFilenameDialog.Filter = "pdf files (*.pdf)|*.pdf";
pdfFilenameDialog.ShowHelp = false;
pdfFilenameDialog.Title = "PDF Scribe - Set output filename";
pdfFilenameDialog.ValidateNames = true;
if (pdfFilenameDialog.ShowDialog(dialogOwner) == DialogResult.OK)
{
outputFile = pdfFilenameDialog.FileName;
filenameRetrieved = true;
}
}
dialogOwner.Close();
}
break;
default:
outputFile = GetOutputFilename();
filenameRetrieved = true;
break;
}
}
return filenameRetrieved;
}
private static String GetOutputFilename()
{
String outputFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), defaultOutputFilename);
if (!String.IsNullOrEmpty(Properties.Settings.Default.OutputFile) &&
!String.IsNullOrWhiteSpace(Properties.Settings.Default.OutputFile))
{
if (IsFilePathValid(Properties.Settings.Default.OutputFile))
{
outputFilename = Properties.Settings.Default.OutputFile;
}
else
{
if (IsFilePathValid(Environment.ExpandEnvironmentVariables(Properties.Settings.Default.OutputFile)))
{
outputFilename = Environment.ExpandEnvironmentVariables(Properties.Settings.Default.OutputFile);
}
}
}
else
{
logEventSource.TraceEvent(TraceEventType.Warning,
(int)TraceEventType.Warning,
String.Format("Using default output filename {0}",
outputFilename));
}
return outputFilename;
}
#region myrtille
private static String GetFreeRDPOutputFilename(Process spooler)
{
var filename = string.Empty;
// myrtille print jobs are prefixed by "FREERDPjob" and concatenate the wfreerdp process id and a timestamp, thus should be unique and secure
// the resulting pdf files are deleted once downloaded to the browser
if (spooler != null &&
spooler.StartInfo != null &&
spooler.StartInfo.EnvironmentVariables != null &&
!string.IsNullOrEmpty(spooler.StartInfo.EnvironmentVariables["REDMON_DOCNAME"]) &&
spooler.StartInfo.EnvironmentVariables["REDMON_DOCNAME"].StartsWith("FREERDPjob"))
{
var systemTempPath = Environment.GetEnvironmentVariable("TEMP", EnvironmentVariableTarget.Machine);
var pdfFile = string.Concat(spooler.StartInfo.EnvironmentVariables["REDMON_DOCNAME"], ".pdf");
filename = Path.Combine(systemTempPath, pdfFile);
}
return filename;
}
#endregion
static bool IsFilePathValid(String filePath)
{
bool pathIsValid = false;
if (!String.IsNullOrEmpty(filePath) && filePath.Length <= 260)
{
String directoryName = Path.GetDirectoryName(filePath);
String filename = Path.GetFileName(filePath);
if (Directory.Exists(directoryName))
{
// Check for invalid filename chars
Regex containsABadCharacter = new Regex("["
+ Regex.Escape(new String(System.IO.Path.GetInvalidPathChars())) + "]");
pathIsValid = !containsABadCharacter.IsMatch(filename);
}
}
else
{
logEventSource.TraceEvent(TraceEventType.Warning,
(int)TraceEventType.Warning,
"Output filename is longer than 260 characters, or blank.");
}
return pathIsValid;
}
///
/// Displays up a topmost, OK-only message box for the error message
///
/// The box's caption
/// The box's message
static void DisplayErrorMessage(String boxCaption,
String boxMessage)
{
#region myrtille
// ensure the current process is running in user interactive mode
if (Environment.UserInteractive)
{
MessageBox.Show(boxMessage,
boxCaption,
MessageBoxButtons.OK,
MessageBoxIcon.Error,
MessageBoxDefaultButton.Button1,
MessageBoxOptions.DefaultDesktopOnly);
}
#endregion
}
}
}