Printing bitmaps II: sending a DIB to the printer
by Damon Chandler
Last time, we discussed how to generate a device-independent representation of a bitmap object—that is, how to create a device-independent bitmap (DIB). We also talked a little bit about the format of a DIB and some advantages and disadvantages of a DIB’s device independence. Now that the conceptual groundwork is set, let’s actually do some printing.
Transferring a DIB—the StretchDIBits() function
As I mentioned last time, the main advantage of a DIB’s device independence is that the format of a DIB is well known. Why is this useful? Because the format is universal, you can safely transfer a DIB among different devices. The greatest disadvantage of using a DIB, however, is that you can’t select a DIB into a memory device context, and so you can’t use the BitBlt() or StretchBlt() functions to draw to a DIB, nor can you use these functions to draw a DIB to a device. However, there is an alternative method you can use to render a DIB to a device: the StretchDIBits() function can transfer a DIB to a graphics device. This function is similar to the StretchBlt() function, except whereas the latter accepts a handle to a (memory) device context for the source image, the former accepts a pointer to a BITMAPINFO structure. The following is the declaration of the StretchDIBits() function:
int StretchDIBits( // handle to the target DC HDC hdc, // destination rectangle int XDest, int YDest, int nDestWidth, int nDestHeight, // source rectangle int XSrc, int YSrc, int nSrcWidth, int nSrcHeight, // pointer to the DIB's pixels CONST VOID* lpBits, // pointer to the DIB's BITMAPINFO CONST BITMAPINFO* lpBitsInfo, // color-table format identifier UINT iUsage, // ternary ROP code DWORD dwRop );
The hdc parameter specifies the destination device context—you use this parameter to tell the StretchDIBits() function which device you want the DIB transferred to.
The XDest, YDest, nDestWidth, and nDestHeight parameters specify, in logical coordinates, a bounding rectangle to which the DIB should stretch to fit. Likewise, the XSrc, YSrc, nSrcWidth, and nSrcHeight parameters specify, in pixels, the rectangular portion of the DIB that you want rendered.
The lpBits parameter specifies a pointer to the DIB’s pixels and the lpBitsInfo parameter specifies the address of the DIB’s corresponding BITMAPINFO structure.
The iUsage parameter specifies the type of color table that the DIB contains. Recall from last time that a DIB’s color table can consist of either RGBQUAD-type entries or WORD-type entries. If the DIB’s color table contains RGBQUADs, you specify iUsage as DIB_RGB_COLORS. If the DIB’s color table contains WORDs, you specify iUsage as DIB_PAL_COLORS. And, if the DIB has no color table, you can specify either identifier.
The dwRop parameter specifies an ROP3 code. In most cases, you’ll want to set dwRop to SRCCOPY; this tells the StretchDIBits() function to simply copy the DIB to the target device, ignoring the colors of the destination and brush. (Note that many printers support only the SRCCOPY mode.)
If the StretchDIBits() function is successful, it returns a value that indicates the number of scan lines that were transferred to the device. If the function fails, it returns GDI_ERROR.
Using the StretchDIBits() function
Before we actually use the StretchDIBits() function to transfer a DIB to a printer, let’s look at an example that demonstrates how to draw a DIB to the screen. The first step, of course, is to actually create the DIB. I’ll repeat the code from last time for convenience, here, wrapped in a function called CreateDIB():
LPBITMAPINFO CreateDIB(
IN const Graphics::TBitmap& Bitmap,
OUT unsigned char*& pBits
)
{
//
// use the GetDIBSizes() function to
// gauge the size of the DIB's parts
//
int header_ct_size = 0;
int img_size = 0;
GetDIBSizes(
const_cast<Graphics::TBitmap&>
(Bitmap).Handle,
header_ct_size, img_size
);
// compute the total size
const size_t total_size =
header_ct_size + img_size;
if (total_size > 0)
{
//
// memory for the header
// and the color table
//
unsigned char* pHeaderAndCT =
new unsigned char[header_ct_size];
// memory for the pixels
pBits = new unsigned char[img_size];
try
{
//
// use the GetDIB() function
// to get the header, color table
// and a copy of Bitmap's pixels
//
bool got_dib =
GetDIB(
const_cast<Graphics::TBitmap&>
(Bitmap).Handle,
const_cast<Graphics::TBitmap&>
(Bitmap).Palette,
pHeaderAndCT, pBits
);
if (got_dib)
{
return reinterpret_cast
<LPBITMAPINFO>(pHeaderAndCT);
}
}
catch (...)
{
// clean up
delete [] pHeaderAndCT;
delete [] pBits;
throw;
}
}
return NULL;
}
Drawing a DIB to a display device context
Contrary to its name, the StretchDIBits() function isn’t limited to rendering a DIB in only a stretched fashion. If you specify a destination rectangle that’s identical to the source rectangle (and the target DC has a 1:1 mapping mode), then the DIB will be rendered in its original size. Let’s look at a bare-bones example. Here, we’ll create and draw a DIB in response to the click of a button. Assume that Image1 contains the bitmap that‘s to be drawn. Here’s the code:
void __fastcall TForm1::Button1Click(
TObject *Sender)
{
Graphics::TBitmap& Bitmap =
*Image1->Picture->Bitmap;
unsigned char* pBits = NULL;
const LPBITMAPINFO pDIB =
CreateDIB(Bitmap, pBits);
if (pDIB && pBits)
{
try
{
const int num_lines =
StretchDIBits(
// destination DC
Canvas->Handle,
// destination rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source image
pBits, pDIB,
// color table type
DIB_RGB_COLORS,
// ROP3 code
SRCCOPY
);
if (num_lines !=
abs(pDIB->bmiHeader.biHeight))
{
throw
Exception("!StretchDIBits");
}
}
catch (...)
{
delete [] pDIB;
delete [] pBits;
throw;
}
delete [] pDIB;
delete [] pBits;
}
}
Note that we passed DIB_RGB_COLORS as the iUsage parameter to the StretchDIBits() function. This specification is indeed valid in our case because the GetDIB() function (which we called from within the CreateDIB() function) always creates a DIB with an RGBQUAD-type color table entry.
If you’ve used the TCanvas::CopyRect() method before, you’ve probably noticed that the StretchDIBits() function isn’t that much different. In contrast to the StretchDIBits() function, however, the CopyRect() method takes care of the palette-related issues when needed (i.e., on palette-based displays). So, before we move on to printing, let’s modify this example to account for palettized displays. We do this by using a combination of the GetDeviceCaps(), SelectPalette(), RealizePalette() functions, and by using the TBitmap::Palette property, like so:
void __fastcall TForm1::Button1Click(
TObject *Sender)
{
Graphics::TBitmap& Bitmap =
*Image1->Picture->Bitmap;
unsigned char* pBits = NULL;
const LPBITMAPINFO pDIB =
CreateDIB(Bitmap, pBits);
if (pDIB && pBits)
{
try
{
// is the display palette-based?
bool uses_palette =
GetDeviceCaps(
Canvas->Handle, RASTERCAPS
) & RC_PALETTE;
HPALETTE hOldPal;
if (uses_palette)
{
//
// select the bitmap's palette
// into the target DC
//
hOldPal =
static_cast<HPALETTE>(
SelectPalette(
Canvas->Handle,
Bitmap.Palette,
FALSE
)
);
// update the system palette
RealizePalette(Canvas->Handle);
}
const int num_lines =
StretchDIBits(
// destination DC
Canvas->Handle,
// destination rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source image
pBits, pDIB,
// color table type
DIB_RGB_COLORS,
// ROP3 code
SRCCOPY
);
if (uses_palette)
{
// clean up
SelectPalette(
Canvas->Handle, hOldPal, TRUE
);
}
if (num_lines !=
abs(pDIB->bmiHeader.biHeight))
{
throw
Exception("!StretchDIBits");
}
}
catch (...)
{
delete [] pDIB;
delete [] pBits;
throw;
}
delete [] pDIB;
delete [] pBits;
}
}
The GetDeviceCaps() function is used to query various capabilities of a device. Here we specify the RASTERCAPS identifier and look for the RC_PALETTE bit. The presence of this bit indicates that the specified device is palette-based, in which case we use a combination of the SelectPalette() and RealizePalette() functions to update the display’s system palette. Note that these latter two functions are usually (and more appropriately) used in response to the WM_QUERYNEWPALETTE message. As we’ll examine next, in the case that a printer is palette-based, we’ll use the SelectPalette() and RealizePalette() functions in a similar fashion.
Drawing a DIB to a printer device context
Okay, so we’ve seen how to use the StretchDIBits() function to render a DIB to a display DC. How is this function used to print a DIB? Well, it’s the same deal, just a different target DC. This time, instead of passing a handle to our form’s device context as the StretchDIBits() function’s hdc parameter, we pass a handle to a device context that refers to a specific printer. For example, by using the TPrinter class, this can be done like so:
#include <printers.hpp>
void __fastcall TForm1::Button1Click(
TObject *Sender)
{
Graphics::TBitmap& Bitmap =
*Image1->Picture->Bitmap;
unsigned char* pBits = NULL;
const LPBITMAPINFO pDIB =
CreateDIB(Bitmap, pBits);
if (pDIB && pBits)
{
// start the print job
Printer()->BeginDoc();
try
{
// is the printer palette-based?
bool uses_palette =
GetDeviceCaps(
Printer()->Canvas->Handle,
RASTERCAPS
) & RC_PALETTE;
HPALETTE hOldPal;
if (uses_palette)
{
//
// select the bitmap's palette
// into the target DC
//
hOldPal =
static_cast<HPALETTE>(
SelectPalette(
Printer()->Canvas->Handle,
Bitmap.Palette,
FALSE
)
);
// update the printer's palette
RealizePalette(Canvas->Handle);
}
const int num_lines =
StretchDIBits(
// destination DC
Printer()->Canvas->Handle,
// destination rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source image
pBits, pDIB,
// color table type
DIB_RGB_COLORS,
// ROP3 code
SRCCOPY
);
if (uses_palette)
{
// clean up
SelectPalette(
Printer()->Canvas->Handle,
hOldPal, TRUE
);
}
// end the print job
Printer()->EndDoc();
if (num_lines !=
abs(pDIB->bmiHeader.biHeight))
{
throw
Exception("!StretchDIBits");
}
}
catch (...)
{
Printer()->EndDoc();
delete [] pDIB;
delete [] pBits;
throw;
}
delete [] pDIB;
delete [] pBits;
}
}
Note that this code is virtually identical to that of the previous code snippets. The only difference is that here we use the TPrinter::BeginDoc() and EndDoc() methods, and we specify Printer()->Canvas->Handle instead of Canvas->Handle (i.e., Form1->Canvas->Handle). In fact, that’s pretty much all there is to printing a bitmap. This approach will succeed on most printers. If you do execute this code, however, you’ll likely get a very small print depending on the resolution of your printer and the dimensions of your bitmap. The reason is that we specified the destination rectangle (i.e., the XDest, YDest, nDestWidth, and nDestHeight parameters) in terms of the bitmap’s actual dimensions. Usually, you’ll want the printed bitmap to be of a certain number of inches or to optimally fit within a page.
To determine the correct scaling factor for the destination rectangle, you can use the TPrinter::PageWidth and PageHeight properties; or you can use the GetDeviceCaps() function. Or you can use a combination of these methods. For example, to stretch the bitmap to the size of the paper (i.e., to occupy the entire printable area), you use the PageWidth and PageHeight properties as follows:
const SIZE SPrint = {
Printer()->PageWidth,
Printer()->PageHeight
};
const int num_lines =
StretchDIBits()(
// destination DC
Printer()->Canvas->Handle,
// destination rectangle
0, 0,
SPrint.cx, SPrint.cy,
// source rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source image
pBits, pDIB,
// color table type
DIB_RGB_COLORS,
// ROP3 code
SRCCOPY
);
Similarly, if you want the printed bitmap as large as possible (on a single page) while preserving the bitmap’s aspect ratio, you use a combination of PageWidth, PageHeight, and GetDeviceCaps(), like so:
// pixels per inch horizontally
const int pix_inchX =
GetDeviceCaps(
Printer()->Canvas->Handle,
LOGPIXELSX
);
// pixels per inch vertically
const int pix_inchY =
GetDeviceCaps(
Printer()->Canvas->Handle,
LOGPIXELSY
);
SIZE SPrint;
SPrint.cx = Printer()->PageWidth;
SPrint.cy =
SPrint.cx * pix_inchY *
abs(pDIB->bmiHeader.biHeight) /
(pix_inchX *pDIB->bmiHeader.biWidth);
const int num_lines =
StretchDIBits()(
// destination DC
Printer()->Canvas->Handle,
// destination rectangle
0, 0,
SPrint.cx, SPrint.cy,
// source rectangle
0, 0,
pDIB->bmiHeader.biWidth,
abs(pDIB->bmiHeader.biHeight),
// source image
pBits, pDIB,
// color table type
DIB_RGB_COLORS,
// ROP3 code
SRCCOPY
);
Paperless proofing
Unless you’re extremely lucky or you have a knack for getting things right the first time, it’ll probably take a few tries to get your printing code fine-tuned. Although you can proof some aspects by simply sending the output (of StretchDIBits()) to your form’s DC instead of to your printer’s DC, it’s a bit difficult to verify positioning and sizing related code without seeing a printed page. After wasting a few stacks of paper, I finally realized that there’s a simple way to gauge (at least approximately) what the printed output will look like—both in terms of position and dimensions—without actually producing a hard copy.
If you go to this URL: http://www.cs.wisc.edu/~ghost/, you can download Ghostscript and GSview. Together, these programs allow you to view Postscript files on screen. This way, you can test your printing code by first displaying a Print common dialog box, and then checking the “Print to file” option. When you confirm the Print dialog box, you’ll be asked to specify a destination file name; your “printed” output will then be directed to the file that you specify. After you’ve printed to the file, you can open it in GSView to see what the output looks like. Of course, for this to work, you’ll need a Postscript printer driver. If you already have a Postscript printer then you’re good to go. Otherwise, pop in your Windows CD-ROM and install a Postscript printer driver (you don’t need an actual printer for this to work). I’ve found the Tektronix Phaser 540 driver to work particularly well.
Conclusion
Is the StretchDIBits() function the only way to print a bitmap? Actually, no. I pointed out last time that there are two functions for transferring a DIB to a device: StretchDIBits() and SetDIBitsToDevice(). The SetDIBitsToDevice() function is similar to StretchDIBits() except the former doesn’t allow stretching. Moreover, the SetDIBitsToDevice() function is supported by fewer devices, so in many cases, the work is simply punted to the StretchDIBits() function.
Next time, I’ll discuss how to make the StretchDIBits-based approach a bit more robust by adding support for a technique called banding. I’ll also show you how to use ICM (Image Color Management) technology to ensure the fidelity of a printed image’s colors.