65.9K
CodeProject 正在变化。 阅读更多。
Home

打印到 TIFF

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (26投票s)

2010 年 9 月 24 日

CPOL

7分钟阅读

viewsIcon

219081

downloadIcon

5215

使用打印驱动程序将任何文档转换为 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。请按照以下步骤操作:

  1. 下载 Windows Driver Kit
  2. 它是一个 ISO 文件,您需要将其刻录到 CD 上,或者使用某个实用工具来挂载该映像。
  3. 安装 WDK。请确保选中了完整的开发环境。
  4. 我建议将示例复制到一个您存储其他代码的地方(并且您定期备份,对吧?)。将 <WinDDK-dir>\src\print\oemdll\bitmap 复制到 <your-dir>\Bitmap_Driver (确保路径和文件名中没有空格!)。
  5. Bitmap_Driver 中创建一个目录,例如“Pack”。这将是用于将驱动程序分发到 CD/DVD/USB/HD/FD/... 的目录。
  6. 将一些 WinDDK 文件从 [...]src\print\oemdll 复制到新目录 [...]\Bitmap_Driver\Pack
    • bitmap.gpd
    • bitmap.inf
    • bitmap.ini
  7. makefile.inc 中,将第一行更改为“INSTALLDIR=.\Pack\bitmap\”(不含引号)。
  8. 现在您可以第一次构建该示例了。

构建驱动程序

正确的构建环境取决于您想在哪里使用它。在开始菜单中,选择 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.hintrface.cppddihook.cppprecomp.h 和 sources。

简而言之,这就是打印时发生的情况:

  1. COemUni2::EnablePDEV - 初始化所有内容
  2. OEMStartPage - 新页面开始时调用的挂钩
  3. OEMSendPage - 通常不会被调用。我们不使用它。
  4. COemUni2::ImageProcessing - 在每个数据块之后
  5. COemUni2::FilterGraphics - 我们不使用
  6. OEMEndDoc - 在最后一页之后。在此我们将数据发送到打印子系统
  7. 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

安装打印机

现在让我们安装打印机并进行尝试。

  1. 添加本地打印机
  2. 使用现有端口:FILE: (打印到文件)
  3. 从磁盘安装...
  4. 浏览到 <your-dir>\Bitmap_Driver\Pack,然后选择 bitmap.inf
  5. 如果不是第一次安装:替换当前驱动程序
  6. 下一步,下一步,完成

现在打印一些内容到新安装的打印机。

如果无法正常工作

日志记录

您可能已经注意到,在某些点会出现一行,例如

log(L"Bitmap->Save failed (frame: %u)", pOemPDEV->m_iframe);	

由于它是一个驱动程序,您不能直接写入屏幕。此外,调试并不容易。这就是为什么我添加了一个函数来使用 OutputDebugStringW 写入消息。您可能知道,在 Windows 7 中要查看消息需要做一些额外的工作。

  1. 使用 RegEdit:导航到 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
  2. 将 DEFAULT 的值更改为 0xf (REG_DWORD)。
  3. 重启
  4. 您可以使用 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 日 - 修复了“入门”中的错误并添加了“要点”。
© . All rights reserved.