打印到 TIFF






4.92/5 (26投票s)
使用打印驱动程序将任何文档转换为 TIFF。

引言
我个人不是很喜欢 PDF。对我来说,它不够“开放”,体积庞大,而且通常无法编辑。我们经常收到 PDF 文件形式的文档,例如工资单和银行票据。为了方便我的管理,我最近努力将纸质文档迁移到计算机。我想使用“开放标准”,例如 TIFF 和 HTML。PDF 根本不符合要求。因此,我需要一种方法将 PDF 转换为 TIFF。这就是这个代码的作用。
背景
我花了些时间寻找转换 PDF 到 TIFF 的代码或工具。不幸的是,我只找到了商业产品。而且我不确定购买后它们是否能满足我的需求。于是我开始寻找自己实现的方法。
PDF 和 TIFF 之间存在一个很大的区别,尤其是其中之一。PDF 文档是“渲染”出来的,而 TIFF 则是一堆像素。这意味着,如果您在 PDF 查看器中放大页面,它仍然保持清晰。另一方面,TIFF 图像的分辨率是有限的。当然,您可以对 PDF 文档进行屏幕截图,但您不会获得比设备(显示器)大一部分更好的分辨率(像素数量)。我没有找到让 PDF 查看器在大于物理屏幕的设备上下文上进行绘制的方法。幸运的是,微软提供了一个打印机驱动程序示例,更幸运的是,一段时间以来,您可以下载包含该示例的 Windows Driver Kit。虽然代码在用户模式下运行,但它确实是一个驱动程序。因此,您需要使用 Windows Driver Kit - Build Environment 来构建它。
使用打印机驱动程序,像素数量可以非常高。至少足以打印在普通纸张上。此外,通过更改应用程序中的打印机属性,您可以更改 DPI、尺寸和颜色数量。更重要的是:现在您可以将几乎所有内容转换为 TIFF,只要应用程序可以打印。
入门
微软的示例是一个工作的驱动程序。它将页面打印到单个位图(.BMP)。使用 TIFF 的原因之一是单个文件可以包含多个页面。因此,必须向示例代码添加代码,以便单独写入每个页面,并以 TIFF 格式而非 BMP 格式写入。
要开始,您需要准备好 Windows Driver Kit。请按照以下步骤操作:
- 下载 Windows Driver Kit。
- 它是一个 ISO 文件,您需要将其刻录到 CD 上,或者使用某个实用工具来挂载该映像。
- 安装 WDK。请确保选中了完整的开发环境。
- 我建议将示例复制到一个您存储其他代码的地方(并且您定期备份,对吧?)。将 <WinDDK-dir>\src\print\oemdll\bitmap 复制到 <your-dir>\Bitmap_Driver (确保路径和文件名中没有空格!)。
- 在
Bitmap_Driver
中创建一个目录,例如“Pack
”。这将是用于将驱动程序分发到 CD/DVD/USB/HD/FD/... 的目录。 - 将一些 WinDDK 文件从 [...]src\print\oemdll 复制到新目录 [...]\Bitmap_Driver\Pack
- bitmap.gpd
- bitmap.inf
- bitmap.ini
- 在 makefile.inc 中,将第一行更改为“INSTALLDIR=.\Pack\bitmap\”(不含引号)。
- 现在您可以第一次构建该示例了。
构建驱动程序
正确的构建环境取决于您想在哪里使用它。在开始菜单中,选择 Windows Driver Kits,WDK 7600.[...],Windows <您选择的系统>,x<您选择的架构> Free Build Environment。这将打开一个具有正确设置的 cmd 窗口。在该窗口中,切换到您的 Bitmap_Driver 目录。然后键入“build”(不含引号)。您的驱动程序(bitmap.dll)将在 Pack\bitmap\<architecture> 目录中。此路径在驱动程序文件 bitmap.inf 中定义。
Using the Code
现在是时候添加代码以使其写入多页 TIFF 了。需要修改以下文件:bitmap.h、intrface.cpp、ddihook.cpp、precomp.h 和 sources。
简而言之,这就是打印时发生的情况:
COemUni2::EnablePDEV
- 初始化所有内容OEMStartPage
- 新页面开始时调用的挂钩OEMSendPage
- 通常不会被调用。我们不使用它。COemUni2::ImageProcessing
- 在每个数据块之后COemUni2::FilterGraphics
- 我们不使用OEMEndDoc
- 在最后一页之后。在此我们将数据发送到打印子系统COemUni2::DisablePDEV
每一页都分一次或多次打印。渲染完一个块后,会调用 intrface.cpp 中的 COemUni2::ImageProcessing
方法。此方法会增加一个缓冲区并添加新的图形数据。该示例将一个挂钩放置在 OEMEndDoc
事件上。在此挂钩函数中,缓冲区被返还给“打印子系统”,该子系统实际上会将其写入您单击打印时指定的那个文件。
因为我们想写入多页 TIFF,所以我们需要在新页面开始时得到通知。因此,我们将挂钩 OEMStartPage
。您可能会认为 OEMSendPage
会更有意义,但事实证明它通常不会被调用。
static const> DRVFN s_aOemHookFuncs[] = {
{INDEX_DrvEndDoc, (PFN)OEMEndDoc},
{INDEX_DrvStartPage, (PFN) OEMStartPage},
{INDEX_DrvSendPage, (PFN) OEMSendPage}
};
当 OEMStartPage
被调用时,我们就开始了一个新页面。此时,我们必须写入前一页。
// --- Write the previous page - if exists ---
if (pOemPDEV->pBufStart) {
if (SaveFrame( pOemPDEV, pDevObj, false))
pOemPDEV->m_iframe+=1;
}
然后初始化一个新的页面
// --- New page ---
// Initializing private oempdev stuff
pOemPDEV->bHeadersFilled = FALSE;
pOemPDEV->bColorTable = FALSE;
pOemPDEV->cbHeaderOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
pOemPDEV->bmInfoHeader.biHeight = 0;
pOemPDEV->bmInfoHeader.biSizeImage = 0;
pOemPDEV->pBufStart = NULL;
pOemPDEV->dwBufSize = 0;
SaveFrame
是一个新函数,用于将页面写入缓冲区。在这里,我们需要放置一些代码,将缓冲区作为一页写入 TIFF 文件。此时,该文件实际上是一个内存流,因为在结束时(OEMEndDoc
)我们必须将数据直接传递给假脱机程序。
/*******************************************************\
*
* Save Frame
*
\*******************************************************/
BOOL SaveFrame(POEMPDEV pOemPDEV, PDEVOBJ pdevobj, BOOL fClose) {
INT cScans;
Gdiplus::Bitmap* pbmp = NULL; //second+ frames: local bitmap
log( L"SaveFrame");
// --- Number of scanlines ---
cScans = pOemPDEV->bmInfoHeader.biHeight;
// --- Flip the biHeight member so that it denotes top-down bitmap ---
pOemPDEV->bmInfoHeader.biHeight = cScans * -1;
BITMAPINFO* pbmpinf = (BITMAPINFO*) LocalAlloc( LPTR,
sizeof(BITMAPINFOHEADER) + (pOemPDEV->cPalColors * sizeof(ULONG)));
CopyMemory( &pbmpinf->bmiHeader, &pOemPDEV->bmInfoHeader,
sizeof(BITMAPINFOHEADER));
if (pOemPDEV->bColorTable) {
CopyMemory( &pbmpinf->bmiColors, pOemPDEV->prgbq,
pOemPDEV->cPalColors*sizeof(RGBQUAD));
LocalFree(pOemPDEV->prgbq);
}
Gdiplus::Status sstat;
log(L"init Gdiplus::Bitmap");
if (!pOemPDEV->m_pbmp) //Make Bitmap for the first time
pOemPDEV->m_pbmp = new Gdiplus::Bitmap( pbmpinf, pOemPDEV->pBufStart);
else //Second time: local bitmap
pbmp = new Gdiplus::Bitmap( pbmpinf, pOemPDEV->pBufStart);
if ((pOemPDEV->m_pbmp) || (pbmp)) {
// --- Encoder Parameters ---
Gdiplus::EncoderParameters* pEncoderParameters =
(Gdiplus::EncoderParameters*)
LocalAlloc( LPTR, sizeof(Gdiplus::EncoderParameters) +
2*sizeof(Gdiplus::EncoderParameter));
ULONG parameterValue0;
ULONG parameterValue1;
// An EncoderParameters object has an
// array of EncoderParameter objects.
pEncoderParameters->Count = 2;
pEncoderParameters->Parameter[0].Guid = Gdiplus::EncoderCompression;
pEncoderParameters->Parameter[0].Type =
Gdiplus::EncoderParameterValueTypeLong;
pEncoderParameters->Parameter[0].NumberOfValues = 1;
pEncoderParameters->Parameter[0].Value = ¶meterValue0;
pEncoderParameters->Parameter[1].Guid = Gdiplus::EncoderSaveFlag;
pEncoderParameters->Parameter[1].Type =
Gdiplus::EncoderParameterValueTypeLong;
pEncoderParameters->Parameter[1].NumberOfValues = 1;
pEncoderParameters->Parameter[1].Value = ¶meterValue1;
//Black n White: CCITT4, else LZW
if (pOemPDEV->bmInfoHeader.biBitCount == 1)
parameterValue0 = Gdiplus::EncoderValueCompressionCCITT4;
else
parameterValue0 = Gdiplus::EncoderValueCompressionLZW;
CLSID clsid;
GetEncoderClsid(L"image/tiff", &clsid);
log(L"Save (frame: %u)", pOemPDEV->m_iframe);
if (pOemPDEV->m_iframe == 0) { //First frame
parameterValue1 = Gdiplus::EncoderValueMultiFrame;
pOemPDEV->m_pbmp->SetResolution(
pdevobj->pPublicDM->dmPrintQuality,
pdevobj->pPublicDM->dmYResolution);
sstat = pOemPDEV->m_pbmp->Save( pOemPDEV->m_pstm,
&clsid, pEncoderParameters);
}
else { //Second+ frame
parameterValue1 = Gdiplus::EncoderValueFrameDimensionPage;
//TODO: Necessary?
pbmp->SetResolution(
pdevobj->pPublicDM->dmPrintQuality,
pdevobj->pPublicDM->dmYResolution);
sstat = pOemPDEV->m_pbmp->SaveAdd( pbmp, pEncoderParameters);
if (pbmp)
delete [] pbmp;
}
if (sstat != Gdiplus::Ok) log(L"Bitmap->
Save failed (frame: %u)", pOemPDEV->m_iframe);
if (fClose) {
// Finishing:
parameterValue1 = Gdiplus::EncoderValueFlush;
sstat = pOemPDEV->m_pbmp->SaveAdd(pEncoderParameters);
}
LocalFree( pEncoderParameters);
}
else
log(L"Couldn\'t make Gdiplus::Bitmap");
LocalFree( pbmpinf);
return true;
}
正如您所见,我们使用 Gdiplus 来完成工作。为什么第一个框架使用成员变量来存储位图,而后续框架使用局部变量?当我们要将页面添加到第一个页面时,我们使用初始位图上的方法,因此我们必须重用它。
为了干净地关闭文件,在结束时(OEMEndDoc
),函数被指示通过设置参数 fClose
来写入 EncoderValueFlush
。
安装打印机
现在让我们安装打印机并进行尝试。
- 添加本地打印机
- 使用现有端口:FILE: (打印到文件)
- 从磁盘安装...
- 浏览到 <your-dir>\Bitmap_Driver\Pack,然后选择 bitmap.inf
- 如果不是第一次安装:替换当前驱动程序
- 下一步,下一步,完成
现在打印一些内容到新安装的打印机。
如果无法正常工作
日志记录
您可能已经注意到,在某些点会出现一行,例如
log(L"Bitmap->Save failed (frame: %u)", pOemPDEV->m_iframe);
由于它是一个驱动程序,您不能直接写入屏幕。此外,调试并不容易。这就是为什么我添加了一个函数来使用 OutputDebugStringW
写入消息。您可能知道,在 Windows 7 中要查看消息需要做一些额外的工作。
- 使用 RegEdit:导航到 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
- 将 DEFAULT 的值更改为 0xf (
REG_DWORD
)。 - 重启
- 您可以使用
DbgView
(以前的 Sysinternals)并通过选择“Capture Global Win32”来查看消息。
现在您可以在代码中添加日志记录了。
重新安装新版本后没有变化
Windows 从驱动程序缓存中提取相同的旧驱动程序。要安装新驱动程序,请更改 bitmap.inf 中的驱动程序版本,然后重新安装。现在将安装新驱动程序。
错误 0x000003eb
如果您尝试安装驱动程序,会出现此错误。在这种情况下,bitmap.gpd 文件可能存在错误。如果您对其进行了更改,请尝试将其替换为原始文件。
关注点
由于 Gdiplus 用于保存文件,因此很容易让驱动程序将其保存为 JPEG、GIF 或其他类型的文件。事实上,这与更改行 GetEncoderClsid(L"image/tiff", &clsid);
差不多一样简单。但是:其他格式不支持一个文件中包含多个页面。因此,如果您需要其他文件格式,最好从原始示例开始,然后添加代码使用 Gdiplus 保存文件(在 OEMEndDoc(...)
中)。另外,检查依赖项,例如 sources 文件中的:“gdiplus.lib”,以及 precomp.h 中的:“gdiplus.h”。
历史
- 2010 年 9 月 24 日 - 初始发布
- 2010 年 9 月 29 日 - 修复了“入门”中的错误并添加了“要点”。