我有一个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");
}
}
问题是,我也希望通过给出文件目标路径而不显示任何PrintDialog
对话来支持虚拟/文件打印机,即PDF打印,但这似乎不适用于。
我真的很想尽量避免第三方库,所以至少现在,使用像PdfSharp
这样的东西把XPS转换成PDF不是我想做的事情。校正:似乎从PdfSharp的最新版本中删除了对转换的支持。
在进行了一些研究之后,直接打印到文件的唯一方法似乎是使用PrintDocument
在PrinterSettings
对象中设置PrintFileName
和PrintToFile
,但是没有办法给出实际的文档内容,而是需要订阅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
上的保存?
谢谢!
编辑:可以使用Microsoft.Office.Interop.Word
直接从Word文档打印到PDF,但是似乎没有简单的方法将XPS/FixedDocument转换为Word。
编辑:到目前为止,最好的方法是抓取PdfSharp 1.31中存在的PdfSharp到PDF的转换代码。我抓取了源代码并构建了它,导入了DLL,它就能工作了。信用归于Nathan,查看他关于这个这里的博客文章。
发布于 2019-10-25 22:38:37
解决了!在谷歌搜索之后,我受到了直接调用Windows打印机的P/Invoke方法的启发。
因此,解决方案是使用打印假脱机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文件。
注释:简单地使用FileStream
读取FileStream
文件将无法工作。我们必须从内存中的XpsDocument
中创建一个Package
,然后从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");
}
https://stackoverflow.com/questions/58517394
复制相似问题