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:
- Download the Windows Driver Kit.
- It is an ISO, so you have to burn it on CD, or use some utility to mount the image.
- Install the WDK. Make sure Full Development Environment is selected.
- 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!)
- 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/... - Copy some WinDDK files from [...]src\print\oemdll to the new directory [...]\Bitmap_Driver\Pack:
- bitmap.gpd
- bitmap.inf
- bitmap.ini
- In makefile.inc, change the first line to "INSTALLDIR=.\Pack\bitmap\" (without the quotes)
- 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:
COemUni2::EnablePDEV
- Initialize all stuffOEMStartPage
- Hook which get called when new page startsOEMSendPage
- Usually won't get called. We don't use it.COemUni2::ImageProcessing
- After every chunk of dataCOemUni2::FilterGraphics
- We don't useOEMEndDoc
- After the last page. Here we send the data to the print sub systemCOemUni2::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.
if (pOemPDEV->pBufStart) {
if (SaveFrame( pOemPDEV, pDevObj, false))
pOemPDEV->m_iframe+=1;
}
Then initiate a new one:
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.
BOOL SaveFrame(POEMPDEV pOemPDEV, PDEVOBJ pdevobj, BOOL fClose) {
INT cScans;
Gdiplus::Bitmap* pbmp = NULL;
log( L"SaveFrame");
cScans = pOemPDEV->bmInfoHeader.biHeight;
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) pOemPDEV->m_pbmp = new Gdiplus::Bitmap( pbmpinf, pOemPDEV->pBufStart);
else pbmp = new Gdiplus::Bitmap( pbmpinf, pOemPDEV->pBufStart);
if ((pOemPDEV->m_pbmp) || (pbmp)) {
Gdiplus::EncoderParameters* pEncoderParameters =
(Gdiplus::EncoderParameters*)
LocalAlloc( LPTR, sizeof(Gdiplus::EncoderParameters) +
2*sizeof(Gdiplus::EncoderParameter));
ULONG parameterValue0;
ULONG parameterValue1;
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;
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) { parameterValue1 = Gdiplus::EncoderValueMultiFrame;
pOemPDEV->m_pbmp->SetResolution(
pdevobj->pPublicDM->dmPrintQuality,
pdevobj->pPublicDM->dmYResolution);
sstat = pOemPDEV->m_pbmp->Save( pOemPDEV->m_pstm,
&clsid, pEncoderParameters);
}
else { parameterValue1 = Gdiplus::EncoderValueFrameDimensionPage;
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) {
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.
- Add a local printer
- Use an existing port: FILE: (Print to File)
- Have Disk...
- Browse to <your-dir>\Bitmap_Driver\Pack, and pick bitmap.inf
- If this isn't the first time: Replace the current driver
- 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:
- Using RegEdit: navigate to HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
- Change the value of DEFAULT to 0xf (
REG_DWORD
) - Reboot
- 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