簡體   English   中英

將 FixedDocument/XPS 打印到 PDF 而不顯示文件保存對話框

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

我有一個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 object 中設置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 class 代替,但這也需要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,如果沒有第 3 方庫,似乎無法將其轉換為 PDF。

是否有可能做一個自動填充文件名然后在PrintDialog上單擊保存的 hack?

謝謝!

編輯:可以使用Microsoft.Office.Interop.Word從 Word 文檔直接打印到 PDF ,但似乎沒有從 XPS/FixedDocument 轉換為 Word 的簡單方法。

編輯:到目前為止,似乎最好的方法是將舊 XPS 獲取到 PdfSharp 1.31 中存在的 PDF 轉換代碼。 我抓取了源代碼並構建了它,導入了 DLL,它就可以工作了。 歸功於 Nathan Jones,在此處查看他的博客文章。

解決了。 谷歌搜索后,我受到直接調用 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.

我相信這是可行的,因為 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 文件。

注釋中的注釋:僅使用FileStream讀取 XPS 文件是行不通的。 我們必須從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