Печать FixedDocument / XPS в PDF без отображения диалогового окна сохранения файла - PullRequest
3 голосов
/ 23 октября 2019

У меня есть FixedDocument, который я позволяю пользователю просматривать в графическом интерфейсе WPF, а затем печатать на бумаге, не показывая диалоговое окно печати 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");
    }
}

Проблема в том, что я хочутакже поддерживать виртуальные / файловые принтеры, а именно печать PDF, указав путь к файлу и не показывая никаких диалогов Windows, но это не похоже на PrintDialog.

. Я бы очень хотелизбегайте сторонних библиотек, насколько это возможно, поэтому, по крайней мере, сейчас, использование чего-то вроде PdfSharp для преобразования XPS в PDF - это не то, чем я хочу заниматься. Исправление: Похоже, поддержка преобразования XPS была удалена из последней версии PdfSharp.

После некоторых исследований кажется, что единственный способ печати прямо в файл - использовать PrintDocument, где можно установить PrintFileName и PrintToFile в объекте PrinterSettings, ноневозможно предоставить фактическое содержимое документа, скорее нам нужно подписаться на событие 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?

Спасибо!

РЕДАКТИРОВАТЬ: Возможно печатать напрямую в PDF из документов Word, используя Microsoft.Office.Interop.Word, но, похоже, нет простого способа конвертировать из XPSИсправлен документ в Word.

РЕДАКТИРОВАТЬ: Кажется, до сих пор лучшим способом является получение старого кода преобразования XPS в PDF, который присутствовал в PdfSharp 1.31. Я взял исходный код и собрал его, импортировал DLL, и он работает. Кредит идет к Натану Джонсу, проверьте его сообщение в блоге об этом здесь .

1 Ответ

3 голосов
/ 26 октября 2019

Решено! После поисков я был вдохновлен методом P / Invoke прямого вызова принтеров Windows.

Поэтому решение состоит в том, чтобы использовать функции API очереди печати для непосредственного вызова доступного принтера Microsoft Print to PDF. в Windows (убедитесь, что эта функция установлена!) и предоставляя функции 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 };
                pdfPrintQueue = printServer.GetPrintQueues(flags).SingleOrDefault(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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...