Sha*_*han 11 .net c# printing pdf wpf
我有一个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");
}
}
Run Code Online (Sandbox Code Playgroud)
问题是我还想通过提供文件目标路径而不显示任何 Windows 对话框来支持虚拟/文件打印机,即 PDF 打印,但这似乎不适用于PrintDialog
.
我真的很想尽可能避免使用 3rd 方库,所以至少现在,使用类似PdfSharp
将 XPS 转换为 PDF 的方法不是我想做的事情。更正:似乎从最新版本的 PdfSharp 中删除了 XPS 转换支持。
之后做一些研究,它似乎直接打印到文件是使用的唯一方法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();
Run Code Online (Sandbox Code Playgroud)
然后是PrintPage
我们需要构建文档的位置的处理程序:
private void OnPrintPage(object sender, PrintPageEventArgs e)
{
// What to do here?
}
Run Code Online (Sandbox Code Playgroud)
我认为可以工作的其他事情是使用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();
Run Code Online (Sandbox Code Playgroud)
但它不是 PDF,如果没有 3rd 方库,似乎无法将其转换为 PDF。
是否有可能做一个自动填充文件名然后点击保存的黑客PrintDialog
?
谢谢!
编辑:可以使用 将 Word 文档直接打印为 PDF Microsoft.Office.Interop.Word
,但从 XPS/FixedDocument 转换为 Word 似乎没有简单的方法。
编辑:到目前为止,最好的方法似乎是获取 PdfSharp 1.31 中存在的旧 XPS 到 PDF 转换代码。我获取了源代码并构建了它,导入了 DLL,它可以工作。归功于 Nathan Jones,请在此处查看他的博客文章。
Sha*_*han 11
解决了!在谷歌搜索之后,我受到了直接调用 Windows 打印机的 P/Invoke 方法的启发。
因此,解决方案是使用Print Spooler 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 };
// 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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
用法:
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");
}
Run Code Online (Sandbox Code Playgroud)
在上面的代码中,我没有直接给出打印机名称,而是通过使用驱动程序名称查找打印队列来获取名称,因为我相信它是不变的,而打印机名称实际上可以在 Windows 中更改,我也不知道它是否是受本地化的影响,所以这种方式更安全。
注意:在开始打印操作之前检查可用磁盘空间大小是一个好主意,因为我找不到可靠地找出错误是否是磁盘空间不足的方法。一种想法是将 XPS 字节数组长度乘以一个像 3 这样的幻数,然后检查磁盘上是否有那么多空间。此外,提供空字节数组或带有虚假数据的数组不会在任何地方失败,但会产生损坏的 PDF 文件。
评论中的注意事项:
简单地使用读取 XPS 文件是FileStream
行不通的。我们必须XpsDocument
从Package
内存中的a创建一个,然后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");
}
Run Code Online (Sandbox Code Playgroud)