在不显示文件保存对话框的情况下将 FixedDocument/XPS 打印为 PDF

Posted

技术标签:

【中文标题】在不显示文件保存对话框的情况下将 FixedDocument/XPS 打印为 PDF【英文标题】:Print FixedDocument/XPS to PDF without showing file save dialog 【发布时间】:2020-02-19 09:05:17 【问题描述】:

我有一个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");
    

问题是我还想通过提供文件目标路径而不显示任何 Windows 对话框来支持虚拟/文件打印机,即 PDF 打印,但这似乎不适用于 PrintDialog

我真的很想尽可能避免使用 3rd 方库,所以至少现在,使用 PdfSharp 之类的东西将 XPS 转换为 PDF 并不是我想做的事情。 更正:似乎从最新版本的 PdfSharp 中删除了 XPS 转换支持。

经过一些研究,似乎直接打印到文件的唯一方法是使用PrintDocument,可以在PrinterSettings 对象中设置PrintFileNamePrintToFile,但没有办法要给出实际的文档内容,我们需要订阅 PrintPage 事件并在创建文档的位置进行一些 System.Drawing.Graphics 操作。

这是我尝试过的代码:

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();

然后是我们需要构建文档的PrintPage 的处理程序:

private void OnPrintPage(object sender, PrintPageEventArgs e)

    // What to do here? 

我认为可以使用的其他方法是使用System.Windows.Forms.PrintDialog 类,但这也需要PrintDocument。我能够像这样轻松地创建 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();

但它不是 PDF,如果没有第三方库,似乎无法将其转换为 PDF。

是否有可能做一个自动填充文件名然后点击PrintDialog上的保存的hack?

谢谢!

编辑: 可以使用Microsoft.Office.Interop.Word 从 Word 文档直接打印到 PDF,但似乎没有从 XPS/FixedDocument 转换为 Word 的简单方法。

编辑:目前看来,最好的方法是获取 PdfSharp 1.31 中存在的旧 XPS 到 PDF 转换代码。我抓取了源代码并构建了它,导入了 DLL,它就可以工作了。感谢 Nathan Jones,查看他关于 here 的博客文章。

【问题讨论】:

我记得 Microsoft Print to PDF 没有提供该选项。我的一个解决方法是创建一个 XPS 或 PNG 文件并使用 GhostScript 转换为 PDF。 @NawedNabiZada 我宁愿避免安装第 3 方工具并从我的代码中调用 exe,但感谢您分享知识。 【参考方案1】:

解决了!谷歌搜索后,我受到了直接调用 Windows 打印机的 P/Invoke 方法的启发。

因此解决方案是使用Print Spooler API 函数直接调用Windows 中可用的Microsoft Print to PDF 打印机(但请确保已安装该功能!)并为WritePrinter 函数提供XPS 文件的字节。

我相信这是可行的,因为 Microsoft PDF 打印机驱动程序可以理解 XPS 页面描述语言。这可以通过检查打印队列的IsXpsDevice 属性来检查。

代码如下:

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;
        
    

用法:

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

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

注意:在开始打印操作之前检查可用磁盘空间大小是个好主意,因为我找不到可靠的方法来确定错误是否是磁盘空间不足。一个想法是将 XPS 字节数组的长度乘以 3 之类的幻数,然后检查磁盘上是否有那么多空间。此外,提供一个空字节数组或一个包含虚假数据的数组不会在任何地方失败,但会产生损坏的 PDF 文件。

来自 cmets 的说明: 简单地使用 FileStream 读取 XPS 文件是行不通的。我们必须从内存中的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");

【讨论】:

请@shahin Dohan,你错过了什么吗?我试了很多次,我只有一个空的pdf @youssouf 您确定安装了“Microsoft Print to PDF”功能吗?如果是,也许将您的代码发布在某个地方,我可以在有时间时尝试自己调试代码。损坏/空白的 PDF 也可能由于文件系统错误而出现,例如没有足够的磁盘空间,甚至可能是权限问题。 谢谢。我已经发布了pastebin.com/embed_js/4iwMPSss 我安装了 Microsoft print To PDF 和内存。我以管理员身份启动了应用程序,但没有任何变化(空 PDF 文件)。就我而言,我有一个想要转换为 PDF 的 XPS 文件。 Bug:由于某种原因,生产系统上安装了两台 PDF 打印机。 SingleOrDefault 不喜欢那样。

以上是关于在不显示文件保存对话框的情况下将 FixedDocument/XPS 打印为 PDF的主要内容,如果未能解决你的问题,请参考以下文章

如何在不通知用户的情况下将画布保存为 png 文件,

如何在不保存 MS 访问查询的情况下将查询数据导出到 txt 文件

如何在没有保存/打开对话框的情况下将文件从服务器下载到客户端机器到浏览器中设置的默认文件夹中?

Python 3:如何在不保存在磁盘上的情况下将 pandas 数据帧作为 csv 流上传?

如何在不写入磁盘的情况下将 AWS S3 上的文本文件导入 pandas

如何在不在线公开的情况下将文件存储在 Wordpress 插件中?