首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >打印FixedDocument/XPS到PDF,而不显示文件保存对话框

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

Stack Overflow用户
提问于 2019-10-23 07:15:21
回答 1查看 10.4K关注 0票数 15

我有一个FixedDocument,允许用户在WPF GUI中预览,然后打印到纸上,而不显示任何Windows打印对话框,如下所示:

代码语言:javascript
运行
复制
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");
    }
}

问题是,我也希望通过给出文件目标路径而不显示任何PrintDialog对话来支持虚拟/文件打印机,即PDF打印,但这似乎不适用于。

我真的很想尽量避免第三方库,所以至少现在,使用像PdfSharp这样的东西把XPS转换成PDF不是我想做的事情。校正:似乎从PdfSharp的最新版本中删除了对转换的支持。

在进行了一些研究之后,直接打印到文件的唯一方法似乎是使用PrintDocumentPrinterSettings对象中设置PrintFileNamePrintToFile,但是没有办法给出实际的文档内容,而是需要订阅PrintPage事件并在创建文档的地方执行一些System.Drawing.Graphics操作。

下面是我尝试过的代码:

代码语言:javascript
运行
复制
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处理程序:

代码语言:javascript
运行
复制
private void OnPrintPage(object sender, PrintPageEventArgs e)
{
    // What to do here? 
}

我认为可以工作的其他事情是使用System.Windows.Forms.PrintDialog类,但这也需要一个PrintDocument。我能够像这样轻松地创建一个XPS文件:

代码语言:javascript
运行
复制
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上的保存?

谢谢!

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

编辑:到目前为止,最好的方法是抓取PdfSharp 1.31中存在的PdfSharp到PDF的转换代码。我抓取了源代码并构建了它,导入了DLL,它就能工作了。信用归于Nathan,查看他关于这个这里的博客文章。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2019-10-25 22:38:37

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

因此,解决方案是使用打印假脱机API函数直接调用Windows中可用的Microsoft Print to PDF打印机(确保安装了该功能!)并给WritePrinter函数一个XPS文件的字节。

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

下面是代码:

代码语言:javascript
运行
复制
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;
        }
    }
}

用法:

代码语言:javascript
运行
复制
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文件。

注释:简单地使用FileStream读取FileStream文件将无法工作。我们必须从内存中的XpsDocument中创建一个Package,然后从MemomryStream中读取字节,如下所示:

代码语言:javascript
运行
复制
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");
}
票数 16
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/58517394

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档