How to Change the Culture of a Winforms Application at Runtime

Print FixedDocument/XPS to PDF without showing file save dialog

Solved! After googling around I was inspired by the P/Invoke method of directly calling Windows printers.

So the solution is to use the Print Spooler API functions to directly call the Microsoft Print to PDF printer available in Windows (make sure the feature is installed though!) and giving the WritePrinter function the bytes of an XPS file.

I believe this works because the Microsoft PDF printer driver understands the XPS page description language. This can be checked by inspecting the IsXpsDevice property of the print queue.

The "Microsoft Print to PDF" feature must be installed in Windows for this to work!

Here's the code:

using System;
using System.Linq;
using System.Printing;
using System.Runtime.InteropServices;

public static class PdfFilePrinter
{
private const string PdfPrinterDriveName = "Microsoft Print To PDF";

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
private class DOCINFOA
{
[MarshalAs(UnmanagedType.LPStr)]
public string pDocName;
[MarshalAs(UnmanagedType.LPStr)]
public string pOutputFile;
[MarshalAs(UnmanagedType.LPStr)]
public string pDataType;
}

[DllImport("winspool.drv", EntryPoint = "OpenPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern bool OpenPrinter([MarshalAs(UnmanagedType.LPStr)] string szPrinter, out IntPtr hPrinter, IntPtr pd);

[DllImport("winspool.drv", EntryPoint = "ClosePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern bool ClosePrinter(IntPtr hPrinter);

[DllImport("winspool.drv", EntryPoint = "StartDocPrinterA", SetLastError = true, CharSet = CharSet.Ansi, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern int StartDocPrinter(IntPtr hPrinter, int level, [In, MarshalAs(UnmanagedType.LPStruct)] DOCINFOA di);

[DllImport("winspool.drv", EntryPoint = "EndDocPrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern bool EndDocPrinter(IntPtr hPrinter);

[DllImport("winspool.drv", EntryPoint = "StartPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern bool StartPagePrinter(IntPtr hPrinter);

[DllImport("winspool.drv", EntryPoint = "EndPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern bool EndPagePrinter(IntPtr hPrinter);

[DllImport("winspool.drv", EntryPoint = "WritePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.StdCall)]
private static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, int dwCount, out int dwWritten);

public static void PrintXpsToPdf(byte[] bytes, string outputFilePath, string documentTitle)
{
// Get Microsoft Print to PDF print queue
var pdfPrintQueue = GetMicrosoftPdfPrintQueue();

// Copy byte array to unmanaged pointer
var ptrUnmanagedBytes = Marshal.AllocCoTaskMem(bytes.Length);
Marshal.Copy(bytes, 0, ptrUnmanagedBytes, bytes.Length);

// Prepare document info
var di = new DOCINFOA
{
pDocName = documentTitle,
pOutputFile = outputFilePath,
pDataType = "RAW"
};

// Print to PDF
var errorCode = SendBytesToPrinter(pdfPrintQueue.Name, ptrUnmanagedBytes, bytes.Length, di, out var jobId);

// Free unmanaged memory
Marshal.FreeCoTaskMem(ptrUnmanagedBytes);

// Check if job in error state (for example not enough disk space)
var jobFailed = false;
try
{
var pdfPrintJob = pdfPrintQueue.GetJob(jobId);
if (pdfPrintJob.IsInError)
{
jobFailed = true;
pdfPrintJob.Cancel();
}
}
catch
{
// If job succeeds, GetJob will throw an exception. Ignore it.
}
finally
{
pdfPrintQueue.Dispose();
}

if (errorCode > 0 || jobFailed)
{
try
{
if (File.Exists(outputFilePath))
{
File.Delete(outputFilePath);
}
}
catch
{
// ignored
}
}

if (errorCode > 0)
{
throw new Exception($"Printing to PDF failed. Error code: {errorCode}.");
}

if (jobFailed)
{
throw new Exception("PDF Print job failed.");
}
}

private static int SendBytesToPrinter(string szPrinterName, IntPtr pBytes, int dwCount, DOCINFOA documentInfo, out int jobId)
{
jobId = 0;
var dwWritten = 0;
var success = false;

if (OpenPrinter(szPrinterName.Normalize(), out var hPrinter, IntPtr.Zero))
{
jobId = StartDocPrinter(hPrinter, 1, documentInfo);
if (jobId > 0)
{
if (StartPagePrinter(hPrinter))
{
success = WritePrinter(hPrinter, pBytes, dwCount, out dwWritten);
EndPagePrinter(hPrinter);
}

EndDocPrinter(hPrinter);
}

ClosePrinter(hPrinter);
}

// TODO: The other methods such as OpenPrinter also have return values. Check those?

if (success == false)
{
return Marshal.GetLastWin32Error();
}

return 0;
}

private static PrintQueue GetMicrosoftPdfPrintQueue()
{
PrintQueue pdfPrintQueue = null;

try
{
using (var printServer = new PrintServer())
{
var flags = new[] { EnumeratedPrintQueueTypes.Local };
// FirstOrDefault because it's possible for there to be multiple PDF printers with the same driver name (though unusual)
// To get a specific printer, search by FullName property instead (note that in Windows, queue name can be changed)
pdfPrintQueue = printServer.GetPrintQueues(flags).FirstOrDefault(lq => lq.QueueDriver.Name == PdfPrinterDriveName);
}

if (pdfPrintQueue == null)
{
throw new Exception($"Could not find printer with driver name: {PdfPrinterDriveName}");
}

if (!pdfPrintQueue.IsXpsDevice)
{
throw new Exception($"PrintQueue '{pdfPrintQueue.Name}' does not understand XPS page description language.");
}

return pdfPrintQueue;
}
catch
{
pdfPrintQueue?.Dispose();
throw;
}
}
}

Usage:

public static void FixedDocument2Pdf(FixedDocument fd)
{
// Convert FixedDocument to XPS file in memory
var ms = new MemoryStream();
var package = Package.Open(ms, FileMode.Create);
var doc = new XpsDocument(package);
var writer = XpsDocument.CreateXpsDocumentWriter(doc);
writer.Write(fd.DocumentPaginator);
doc.Close();
package.Close();

// Get XPS file bytes
var bytes = ms.ToArray();
ms.Dispose();

// Print to PDF
var outputFilePath = @"C:\tmp\test.pdf";
PdfFilePrinter.PrintXpsToPdf(bytes, outputFilePath, "Document Title");
}

In the code above, instead of directly giving the printer name, I get the name by finding the print queue using the driver name because I believe it's constant while the printer name can actually be changed in Windows, also I don't know if it's affected by localization so this way is safer.

Note: It's a good idea to check available disk space size before starting the printing operation, because I couldn't find a way to reliably find out if the error was insufficient disk space. One idea is to multiply the XPS byte array length by a magic number like 3 and then check if we have that much space on disk. Also, giving an empty byte array or one with bogus data does not fail anywhere, but produces a corrupt PDF file.

Note from comments:
Simply reading an XPS file using FileStream will not work. We have to create an XpsDocument from a Package in memory, then read the bytes from the MemomryStream like this:

public static void PrintFile(string xpsSourcePath, string pdfOutputPath)
{
// Write XPS file to memory stream
var ms = new MemoryStream();
var package = Package.Open(ms, FileMode.Create);
var doc = new XpsDocument(package);
var writer = XpsDocument.CreateXpsDocumentWriter(doc);
writer.Write(xpsSourcePath);
doc.Close();
package.Close();

// Get XPS file bytes
var bytes = ms.ToArray();
ms.Dispose();

// Print to PDF
PdfPrinter.PrintXpsToPdf(bytes, pdfOutputPath, "Document title");
}

are there some way to print by xps printer without show the save dialog?

It's not possible to control the XPS Document Writer file name from VB6, but it is possible using a third party product named Win2PDF. To set the XPS file name in Win2PDF, you use the SaveSetting method to save the file name to the registry as in:

SaveSetting "Dane Prairie Systems", "Win2PDF", "PDFFileName", "c:\test\test.xps"

After calling SaveSetting with the key set to "PDFFileName", you print to the Win2PDF printer using the VB6 printer object to create the XPS file.

Windows 10: Simple way to print a PDF-file without the save-dialog

Microsoft Print to PDF on Windows is not "Free", simply "Leased", however that said you can change the owners designed behavior to a different "port" than "prompt" or use the drivers to print to your desired named file.

To use ONE fixed output filename like %TEMP%OUT.PDF you are best served by cloning/duplicate the "Microsoft Print to PDF" to a printer name of your choice so I call mine "My Print to PDF" as its shorter to type and the Auto printed file goes to MyData folder. For a visual guide see https://stackoverflow.com/a/69169728/10802527 and up vote there if that helps.

The alternative is to use a structure like

CliPdfApp /PrintTo file.pdf "Microsoft Print to PDF" "Microsoft Print to PDF" "C:\MyFavourite Places\FileName.pdf

However few apps follow the required convention, so WordPad will convert Docx or RTF via command line but can not handle a PDF and Edge AFAIK was not designed to make the PDF format CLI print friendly :-). But those links you have in the question will suggest acrord32 /p or /t filename printer printdriver filename and that is probably the best method for flattening acroforms

Disclaimer I support SumatraPDF so can suggest to "Print As Image ONLY" its perfect as one single 32 or 64 bit portable.exe https://www.sumatrapdfreader.org/prerelease so all you need is:-

SumatraPDF -print-to "My Print to PDF" filename.pdf (or other types supported)

There are other print methods/options but BE-AWARE that is NOT flattening forms since "Flattening" means convert the form to plain readable text and SumatraPDF ONLY prints PDF as Imagery.

So combining SumatraPDF with a promptless port will provide a single command to build a known output then you need to monitor that output and rename to one of your choice, that can be tricky if you are submerged and "running silent and deep" without GDI feedback (that the print is spooling/erroring) and time is as variable as the input PDFs complexity.

You use the word "Slow" but that is the innate feature of PDF "Slow and Steady" output are its designed aims.

As an alternative to SumatraPDF two other viewers are more geared towards PDF Command Line printing. One is Acrobat Reader as per above "/Terminate and Stay Resident" and it does that exceptionally well so should be preferred. A good alternative lightweight but powerful PDF handler is Tracker PDF X-change which has both command line printing and its own programmable printer drivers.



Related Topics



Leave a reply



Submit