Click here to Skip to main content
Click here to Skip to main content

Print to TIFF

, 29 Sep 2010
Rate this:
Please Sign up or sign in to vote.
Convert any document to TIFF using a printer driver

Introduction

Personally I'm not a big fan of PDF. To me, it's not "open", it's big and usually you cannot edit it afterwards. More often we receive documents, like payrolls and bank notes, in those PDF files. For my administration, I recently made some effort to move from paper to computer. I want to use "open standards", like TIFF and HTML. PDF just doesn't fit. So I needed a way to convert PDF to TIFF. That's what this code does.

Background

I searched some time to find some code or tools to convert PDF to TIFF. Unfortunately I could only find commercial products. And I'm not sure that if I buy one, it does what I want. So I looked for ways to do it myself.

There is, among others, a big difference between PDF and TIFF. A PDF document gets "rendered", whereas TIFF is just a bunch of pixels. This means that if you enlarge a page in a PDF viewer, it stays sharp. On the other hand a TIFF image has a limited resolution. Of course you can Alt PrintScreen on a PDF document, but you won't get a better resolution (number of pixels) than a big part of your device (monitor). I didn't find a way to let a PDF Viewer write on a Device Context larger then the Device Context of the physical screen. Luckily, Microsoft provided a Printer Driver sample, and more luckily, since some time you can download the Windows Driver Kit which includes the sample. Though the code runs in user mode, it IS a driver. So you need to build it with the Windows Driver Kit - Build Environment.

Using a Printer Driver, the number of pixels can be very high. At least sufficient for printing on a normal page. Also, by changing the printer properties in the application, you can change the DPI, size and number of colors. And what's more: now you can convert almost anything to TIFF, as long as the application can print.

Getting Started

The Microsoft sample is a working driver. It prints the pages to one bitmap (.BMP). One of the reasons to use TIFF is that one file can contain more than one page. So code must be added to the sample to write each page separately and to write it in TIFF instead of BMP.
To get started, you'll have to get the Windows Driver Kit in place. Follow those steps:

  1. Download the Windows Driver Kit.
  2. It is an ISO, so you have to burn it on CD, or use some utility to mount the image.
  3. Install the WDK. Make sure Full Development Environment is selected.
  4. I suggest to copy the sample to a place where the rest of your code resides (and which you regularly back up, right?). Copy <WinDDK-dir>\src\print\oemdll\bitmap to <your-dir>\Bitmap_Driver (make sure there is no space in the path and name!)
  5. Make a directory in Bitmap_Driver, e.g. "Pack". This will be the directory for distributing the driver to a CD/DVD/USB/HD/FD/...
  6. Copy some WinDDK files from [...]src\print\oemdll to the new directory [...]\Bitmap_Driver\Pack:
    • bitmap.gpd
    • bitmap.inf
    • bitmap.ini
  7. In makefile.inc, change the first line to "INSTALLDIR=.\Pack\bitmap\" (without the quotes)
  8. Now you can build the sample for the first time.

Build the Driver

The right build environment depends on the computer where you want to use it. In start menu, select Windows Driver Kits, WDK 7600.[...], Windows <you choose>, x<you choose> Free Build Environment. This will open a cmd box with the right settings. In this box, switch to your Bitmap_Driver directory. Then type "build" (without the quotes). Your driver (bitmap.dll) will be in the Pack\bitmap\<architecture> directory. This path is defined in the driver file: bitmap.inf.

Using the Code

Now it's time to add the code to make it write to a Multipage TIFF. The following files need to be changed: bitmap.h, intrface.cpp, ddihook.cpp, precomp.h and sources.

In short, this is what happens when printing:

  1. COemUni2::EnablePDEV - Initialize all stuff
  2. OEMStartPage - Hook which get called when new page starts
  3. OEMSendPage - Usually won't get called. We don't use it.
  4. COemUni2::ImageProcessing - After every chunk of data
  5. COemUni2::FilterGraphics - We don't use
  6. OEMEndDoc - After the last page. Here we send the data to the print sub system
  7. COemUni2::DisablePDEV

Every page is printed in one or more parts. After rendering a part, the method COemUni2::ImageProcessing in intrface.cpp is called. This method increases a buffer and adds the new graphics data. The sample places a hook to the event OEMEndDoc. In this hook function, the buffer is given back to the "print sub system", which will actually write it to the file you specified when you clicked Print.

Because we want to write a Multipage TIFF, we need to get notified if a new page starts. Therefore, we are going to hook OEMStartPage. You might expect that OEMSendPage would make more sense, but it turned out that it usually doesn't get called.

static const> DRVFN s_aOemHookFuncs[] = {
{INDEX_DrvEndDoc, (PFN)OEMEndDoc},
{INDEX_DrvStartPage, (PFN) OEMStartPage},
{INDEX_DrvSendPage, (PFN) OEMSendPage}
};

When OEMStartPage gets called, we are beginning a new page. At this moment, we have to write the previous page.

// --- Write the previous page - if exists ---
if (pOemPDEV->pBufStart) {
    if (SaveFrame( pOemPDEV, pDevObj, false))
        pOemPDEV->m_iframe+=1;
    }

Then initiate a new one:

// --- 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 is a new function to write a page to the buffer. Here we have to put some code to write the buffer as a page in a TIFF file. This file is at this stage actually a memory stream, because at the end (OEMEndDoc) we have to pass the data directly to the spooler.

/*******************************************************\
*
* 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;
	}

As you can see, we use Gdiplus to do the work. Why do we use a member variable for the bitmap for the first frame, and then a local variable for the following frames? When we want to add a page to the first one, we use a method on the initial bitmap, so we have to reuse that one.

To close the file cleanly, at the end (OEMEndDoc) the function is instructed to write EncoderValueFlush by setting the parameter fClose.

Install the Printer

Now let's install the printer and try it.

  1. Add a local printer
  2. Use an existing port: FILE: (Print to File)
  3. Have Disk...
  4. Browse to <your-dir>\Bitmap_Driver\Pack, and pick bitmap.inf
  5. If this isn't the first time: Replace the current driver
  6. Next, Next, Finish

Now print something to the new printer.

If You Can't Get It to Work

Logging

As you may have noticed, at some points a line appears like:

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

Because it is a driver, you cannot write directly to the screen. Also, debugging is not that easy. That's why I added a function to write message using OutputDebugStringW. As you may know, you have to do some extra work to view the messages with Windows 7:

  1. Using RegEdit: navigate to HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
  2. Change the value of DEFAULT to 0xf (REG_DWORD)
  3. Reboot
  4. You can view the messages by using DbgView (Formerly Sysinternals), and selecting "Capture Global Win32"

Now you can add logging in the code.

No changes after reinstalling a new version

Windows picks the same old driver out of the driver cache. To install the new one, change the driver version in the bitmap.inf, and reinstall again. Now the new driver will get installed.

Error 0x000003eb

If you try to install the driver, this error appears. In this case, there may be an error in the file bitmap.gpd. If you changed it, try to replace it by the original.

Points of Interest

Because Gdiplus is used to save the file, it is very easy to make the driver save it as a JPEG, GIF or other type of file. In fact, it is about as easy as changing the line GetEncoderClsid(L"image/tiff", &clsid); But: other formats do not support several pages in one file. So, if you need another file format, it is best to start from the original sample, and then add code to save the file using Gdiplus (in OEMEndDoc(...)). Also, check the dependencies in e.g. sources: "gdiplus.lib", and precomp.h: "gdiplus.h"

History

  • 24 September 2010 - Initial release
  • 29 September 2010 - Fixed error in "Getting Started" and added Points of Interest

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Pascal Damman
Architect Tunari
Netherlands Netherlands
No Biography provided

Comments and Discussions

 
Questionvisual c++ 2010 solution? PinmemberYuriMaks15-Jul-13 10:49 
AnswerRe: visual c++ 2010 solution? PinmemberPascal Damman20-Feb-14 2:18 
Questionthanks for sharing Pinmemberdanny rough14-May-13 18:21 
QuestionSpooler memory bloat PinmemberMember 863753423-Apr-13 7:43 
QuestionAcrobat PDF's don't look right Pinmembercbslc13-Mar-13 4:49 
QuestionWindows 8 x64 - v4 driver model Pinmemberhzsombor11-Nov-12 3:37