我正在尝试让我的 Excel VBA 代码读取交互式 PDF(即带有下拉列表、日历等的 PDF)。我通常通过在 Word 中打开 PDF 文件来阅读它们;但是,Word 在呈现文件时会删除所有交互式控件(例如,日历控件会被完全删除……连同它的值)。
我知道我可以通过在 Adobe Acrobat Reader(或任何浏览器)中打开 PDF 并将其打印到 PDF(从而将所有控件替换为其所选值)来“拼合”PDF。问题是我如何在 VBA 中以编程方式执行此操作。我知道关于这个话题有成千上万的问题;但是,所有答案要么需要安装 Adobe Acrobat Pro,使用第三方安装的应用程序(例如,CutePDF),将文件提交到在线 API,或者最常见的是,使用 SendKeys 或 Win32 API 调用与“另存为”交互" 出现的对话框。我正在编写的代码将分发给多个用户;因此,任何额外的应用程序安装都是不可能的(Acrobat Pro 和其他第三方转换工具)。这些文件包含专有数据;所以,一个在线 API 也出来了。
我尝试了很多方法,包括 Chrome 的无头“打印到 PDF”;但似乎只接受 HTML 文件(我什至尝试将 PDF 嵌入到 HTML 文件中,但它仍然不起作用)。我得到的最接近的是使用下面的代码将数据直接发送到“Microsoft Print to PDF”驱动程序(它基于此处描述的过程)。该代码成功创建了一个 PDF 文件……但它是一个零字节文件。“WritePrinter”函数似乎没有正确接受从输入文件读取的数据(尽管没有发生错误)。虽然“WritePrinter”函数的文档说明“pBuf”参数应该是一个字节数组,但问这个问题的人正在传递一个字符串并让它工作(了解他们没有尝试打印到 GDI 打印机)。如果我将字节数组转换为字符串,或者甚至只是使用 FSO 的“ReadAll”方法读取输入文件的内容,我的代码仍然只会生成一个空白 PDF 文件。“WritePrinter”函数的返回值是“1”,而“pcWritten”输出的值是正确的字节数,这也是毫无价值的……只是生成的PDF文件有一个文件大小0字节。
那么,谁能弄清楚如何让“WritePrinter”函数接受从输入文件中读取的数据?
顺便说一句,对于任何能解决这个问题的人来说,这是一颗巨大的金星,因为根据我的研究,互联网正在寻求一种方法来做到这一点,而无需使用 Acrobat Pro 或不必与“另存为”对话框交互!
更新#1:我在网上找到了一些帖子,用户遇到了“Microsoft Print to PDF”驱动程序手动生成空白文件的问题(此处和此处)。显然,罪魁祸首是输入文件名中的特殊字符。我想明确表示我不相信这是我遇到的问题,因为我可以手动完美地打印到 PDF ......只是不是通过这篇文章中的代码。(另外,正如刚才所说,输入文件路径/名称和输出文件路径/名称都不包含特殊字符)。此外,我不认为这是特定安装打印驱动程序的问题(如某些帖子中所建议的那样),因为我的代码在 3 台不同的计算机上创建了 0 字节文件,这些计算机具有 3 种不同的 Windows 操作系统版本(18363.1082、18363.1139 和 19041.572 )
更新#2:在我对这个问题的持续研究中,我在 MSDN 的 Visual Studio 帮助论坛上找到了这篇文章。我知道它适用于 C#,但其中一位贡献者表示:
所以你应该将托管字节数组转换为非托管数组,然后调用该方法
他提供的 C# 代码使用“Marshal.AllocCoTaskMem”和“Marshal.Copy”函数来“将托管字节数组复制到非托管数组中”。我不熟悉术语“托管”或“非托管”字节数组,因此我将继续对这些术语进行一些研究。是否有人对 Excel (VB6) 中的 VBA 中的“Marshal.AllocCoTaskMem”和“Marshal.Copy”函数有任何经验?
更新#3:我注意到我编写的代码只会将 XPS 文件打印到 PDF。我已手动将原始交互式 PDF 转换为 XPS,并确认我的代码在将 XPS 写入 PDF 时运行良好。这让我回到了第一个问题:如何在不使用任何第三方应用程序或在线转换器的情况下以编程方式阅读交互式 PDF?有人有想法么?
Type DOCINFO
lpszDocName As String
lpszOutput As String
lpszDatatype As String
End Type
Private Declare PtrSafe Function OpenPrinter Lib "winspool.drv" Alias "OpenPrinterA" (ByVal pPrinterName As String, phPrinter As Long, ByVal pDefault As Long) As Long
Private Declare PtrSafe Function StartDocPrinter Lib "winspool.drv" Alias "StartDocPrinterA" (ByVal hPrinter As Long, ByVal Level As Long, pDocInfo As DOCINFO) As Long
Private Declare PtrSafe Function StartPagePrinter Lib "winspool.drv" (ByVal hPrinter As Long) As Long
Private Declare PtrSafe Function WritePrinter Lib "winspool.drv" (ByVal hPrinter As Long, pBuf As Any, ByVal cdBuf As Long, pcWritten As Long) As Long
Private Declare PtrSafe Function EndPagePrinter Lib "winspool.drv" (ByVal hPrinter As Long) As Long
Private Declare PtrSafe Function EndDocPrinter Lib "winspool.drv" (ByVal hPrinter As Long) As Long
Private Declare PtrSafe Function ClosePrinter Lib "winspool.drv" (ByVal hPrinter As Long) As Long
Public Sub Print_File_To_PDF(strInputFile As String, strOutputPDF As String)
Dim udtDocInfo As DOCINFO
Dim lngPrinterCheck As Long, lngFileNumber As Long, lngPrinterHandle As Long, lngPrinterOutput as Long
Dim arrBytes() As Byte
'Get handle of printer
lngPrinterCheck = OpenPrinter("Microsoft Print To PDF", lngPrinterHandle, 0)
If lngPrinterCheck = 0 Then
Exit Sub
End If
'Define document info
With udtDocInfo
.lpszDocName = CreateObject("Scripting.FileSystemObject").GetBaseName(strOutputPDF)
.lpszOutput = strOutputPDF
.lpszDatatype = "RAW"
End With
'Read file into byte array
lngFileNumber = FreeFile
Open strInputFile For Binary Access Read As lngFileNumber
ReDim arrBytes(LOF(lngFileNumber))
Get lngFileNumber, , arrBytes()
Close lngFileNumber
'Print byte array to PDF file
Call StartDocPrinter(lngPrinterHandle, 1, udtDocInfo)
Call StartPagePrinter(lngPrinterHandle)
Call WritePrinter(lngPrinterHandle, arrBytes(1), UBound(arrBytes), lngPrinterOutput)
Call EndPagePrinter(lngPrinterHandle)
Call EndDocPrinter(lngPrinterHandle)
Call ClosePrinter(lngPrinterHandle)
End Sub