简体   繁体   English

将 FixedDocument/XPS 打印到 PDF 而不显示文件保存对话框

[英]Print FixedDocument/XPS to PDF without showing file save dialog

I have a FixedDocument that I allow the user to preview in a WPF GUI and then print to paper without showing any Windows printing dialogue, like so:我有一个FixedDocument ,我允许用户在 WPF GUI 中预览,然后打印到纸上而不显示任何 Windows 打印对话框,如下所示:

private void Print()
{
    PrintQueueCollection printQueues;
    using (var printServer = new PrintServer())
    {
        var flags = new[] { EnumeratedPrintQueueTypes.Local };
        printQueues = printServer.GetPrintQueues(flags);
    }

    //SelectedPrinter.FullName can be something like "Microsoft Print to PDF"
    var selectedQueue = printQueues.SingleOrDefault(pq => pq.FullName == SelectedPrinter.FullName);

    if (selectedQueue != null)
    {
        var myTicket = new PrintTicket
        {
            CopyCount = 1,
            PageOrientation = PageOrientation.Portrait,
            OutputColor = OutputColor.Color,
            PageMediaSize = new PageMediaSize(PageMediaSizeName.ISOA4)
        };

        var mergeTicketResult = selectedQueue.MergeAndValidatePrintTicket(selectedQueue.DefaultPrintTicket, myTicket);
        var printTicket = mergeTicketResult.ValidatedPrintTicket;

        // TODO: Make sure merge was OK

        // Calling GetPrintCapabilities with our ticket allows us to use
        // the OrientedPageMediaHeight/OrientedPageMediaWidth properties
        // and the PageImageableArea property to calculate the minimum
        // document margins supported by the printer. Very important!
        var printCapabilities = queue.GetPrintCapabilities(myTicket);
        var fixedDocument = GenerateFixedDocument(printCapabilities);

        var dlg = new PrintDialog
        {
            PrintTicket = printTicket,
            PrintQueue = selectedQueue
        };

        dlg.PrintDocument(fixedDocument.DocumentPaginator, "test document");
    }
}

The problem is that I want to also support virtual/file printers, namely PDF printing, by giving the file destination path and not showing any Windows dialogues, but that doesn't seem to work with the PrintDialog .问题是我还想通过提供文件目标路径而不显示任何 Windows 对话来支持虚拟/文件打印机,即 PDF 打印,但这似乎不适用于PrintDialog

I would really like to avoid 3rd party libraries as much as possible, so at least for now, using something like PdfSharp to convert an XPS to PDF is not something I want to do.我真的很想尽可能避免使用 3rd 方库,所以至少现在,使用PdfSharp之类的东西将 XPS 转换为 PDF 不是我想做的事情。 Correction: It seems like XPS conversion support was removed from the latest version of PdfSharp.更正:似乎从最新版本的 PdfSharp 中删除了 XPS 转换支持。

After doing some research, it seems the only way to print straight to a file is to use a PrintDocument where it's possible to set PrintFileName and PrintToFile in the PrinterSettings object, but there is no way to give the actual document contents, rather we need to subscribe to the PrintPage event and do some System.Drawing.Graphics manipulation where the document is created.在做了一些研究之后,似乎直接打印到文件的唯一方法是使用PrintDocument可以在PrinterSettings object 中设置PrintFileNamePrintToFile ,但是没有办法给出实际的文档内容,而是我们需要订阅PrintPage事件并在创建文档的位置执行一些System.Drawing.Graphics操作。

Here's the code I tried:这是我尝试过的代码:

var printDoc = new PrintDocument
{
    PrinterSettings =
    {
        PrinterName = SelectedPrinter.FullName,
        PrintFileName = destinationFilePath,
        PrintToFile = true
    },
    PrintController = new StandardPrintController()
};

printDoc.PrintPage += OnPrintPage; // Without this line, we get a blank PDF
printDoc.Print();

Then the handler for PrintPage where we need to build the document:然后是我们需要构建文档的PrintPage处理程序:

private void OnPrintPage(object sender, PrintPageEventArgs e)
{
    // What to do here? 
}

Other things that I thought could work are using the System.Windows.Forms.PrintDialog class instead, but that also expects a PrintDocument .我认为可以使用的其他方法是使用System.Windows.Forms.PrintDialog class 代替,但这也需要PrintDocument I was able to create an XPS file easily like so:我能够像这样轻松地创建 XPS 文件:

var pkg = Package.Open(destinationFilePath, FileMode.Create);
var doc = new XpsDocument(pkg);
var writer = XpsDocument.CreateXpsDocumentWriter(doc);
writer.Write(PreviewDocument.DocumentPaginator);
pkg.Flush();
pkg.Close();

But it's not a PDF, and there seems to be no way to convert it to PDF without a 3rd party library.但它不是 PDF,如果没有第 3 方库,似乎无法将其转换为 PDF。

Is it possible to maybe do a hack that automatically fills the filename then clicks save on the PrintDialog ?是否有可能做一个自动填充文件名然后在PrintDialog上单击保存的 hack?

Thank you!谢谢!

EDIT: It's possible to print directly to PDF from Word documents using Microsoft.Office.Interop.Word , but there seems to be no easy way of converting from XPS/FixedDocument to Word.编辑:可以使用Microsoft.Office.Interop.Word从 Word 文档直接打印到 PDF ,但似乎没有从 XPS/FixedDocument 转换为 Word 的简单方法。

EDIT: It seems so far the best way is to grab the old XPS to PDF conversion code that was present in PdfSharp 1.31.编辑:到目前为止,似乎最好的方法是将旧 XPS 获取到 PdfSharp 1.31 中存在的 PDF 转换代码。 I grabbed the source code and built it, imported the DLL's, and it works.我抓取了源代码并构建了它,导入了 DLL,它就可以工作了。 Credit goes to Nathan Jones, check out his blog post about thishere .归功于 Nathan Jones,在此处查看他的博客文章。

Solved.解决了。 After googling around I was inspired by the P/Invoke method of directly calling Windows printers.谷歌搜索后,我受到直接调用 Windows 打印机的 P/Invoke 方法的启发。

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. 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.我相信这是可行的,因为 Microsoft PDF 打印机驱动程序理解 XPS 页面描述语言。 This can be checked by inspecting the IsXpsDevice property of the print queue.这可以通过检查打印队列的IsXpsDevice属性来检查。

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.在上面的代码中,我没有直接给出打印机名称,而是通过使用驱动程序名称查找打印队列来获取名称,因为我相信它是恒定的,而打印机名称实际上可以在 Windows 中更改,我也不知道是不是受本地化影响,因此这种方式更安全。

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.一种想法是将 XPS 字节数组的长度乘以 3 之类的幻数,然后检查磁盘上是否有那么多空间。 Also, giving an empty byte array or one with bogus data does not fail anywhere, but produces a corrupt PDF file.此外,提供一个空字节数组或一个包含虚假数据的数组不会在任何地方失败,但会产生损坏的 PDF 文件。

Note from comments: Simply reading an XPS file using FileStream will not work.注释中的注释:仅使用FileStream读取 XPS 文件是行不通的。 We have to create an XpsDocument from a Package in memory, then read the bytes from the MemomryStream like this:我们必须从Package中的 Package 创建一个XpsDocument ,然后像这样从MemomryStream中读取字节:

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");
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM