Save Client Area of a Window to PNG File

This is offered to anyone who might be able to use it.  It’s plain c code to write the contents of a Microsoft window (from a win32 app) to a png file.  pnglib provides all the primitives necessary, but I’ve put it all together into one simple function.

ClientToPng Source (2.7k)

ClientToPng Precompiled (665k)

The first file above contains the source (shown at the end of this article) if you want to compile it yourself. This is the most reliable way of making it work, but requires that you have a static libpng.lib available to link to. Instructions on how to do that are here.

The second file includes a precompiled static library. It has the kitchen sink in it, so is almost 2 MB when unzipped. However, the only function you will need to call is ClientToPng(), which will add perhaps 100k to the size of your program. You will need to include ClientToPng.h at the top of your program, then make a call something like this:

#define OUTSTR_LEN 255
long status;

// handle of window to save, filename to save to
status = SaveClientAreaToPng(hWnd, L"filename.png");

// the status returned is a standard Windows error
if (status != NO_ERROR) {
    FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, status, 0,
        OutString1, OUTSTR_LEN, NULL);
    MessageBox(hWndMain, OutString1, TEXT("Error"),

You would then link ClientToPng.lib to your program. Although this is theoretically simple, there could be complications if settings that ClientToPng.lib were complied with do not match those of your program. In particular, this library was compiled for x86 (not x64) code, non-debug, non-runtime-dll, under Microsoft Visual Studio 2010 Express. If you need other settings, you’ll have to recompile the source yourself, linking to libpng.lib .

Here is the source.  There were a few parts that were tricky.

  • The buffer size for retrieving pixels from a Microsoft bitmap must be a whole number of strides.
  • The rows in Microsoft bitmaps go from the bottom up, in PNG, they go from the top down.
  • Colors are represented as BGR from Microsoft, and stored as RGB for PNG files.
#include "StdAfx.h"
#include "png.h"
#include "ClientToPng.h"

// Save client area of supplied hWnd to filename.png
// Warning!  filename is probably a WCHAR string, depending on compiler settings
// Assumptions:
// 1) Compatible bitmap has bottom-up orientation
// 2) Compatible bitmap will return colors in BGR instead of RGB format
// 3) Client Rect has lower bound of 0 in both x and y dimension
// 4) Bitmap it not palletized
// Return value: 0 = success, others return standard WinError number
// writing PNG leverged from
long SaveClientAreaToPng(HWND hWnd, TCHAR *filename) {
	long code;  // return code
	FILE *fp = NULL;
	png_structp png_ptr = NULL;
	png_infop info_ptr = NULL;
	png_bytep row = NULL;
	RECT Rect;
	HDC hdc = NULL;
	HDC hDest = NULL;
	HBITMAP hBitmap = NULL;
	HBITMAP hPrevBitmap = NULL;

	// get dimensions of client area to save
	// note that this is generally smaller than the whole window
	GetClientRect(hWnd, &Rect);

	// Open file for writing (binary mode)
	code = _tfopen_s(&fp, filename, TEXT("wb"));
	if (code != NO_ERROR) {
		goto finalise;

	// Initialize write structure
	png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
	if (png_ptr == NULL) {
		goto finalise;

	// Initialize info structure
	info_ptr = png_create_info_struct(png_ptr);
	if (info_ptr == NULL) {
		goto finalise;

	// Setup Exception handling
	// Any subsequent errors in png_xxxx calls will cause this
	// to execute
	if (setjmp(png_jmpbuf(png_ptr))) {
		code = ERROR_INVALID_DATA;  // best match standard error message
		goto finalise;

	png_init_io(png_ptr, fp);

	// Write header (8 bit colour depth)
	png_set_IHDR(png_ptr, info_ptr, Rect.right, Rect.bottom,

	// Set title
	//if (title != NULL) {
	//	png_text title_text;
	//	title_text.compression = PNG_TEXT_COMPRESSION_NONE;
	//	title_text.key = "Title";
	//	title_text.text = title;
	//	png_set_text(png_ptr, info_ptr, &title_text, 1);

	png_write_info(png_ptr, info_ptr);

	// Now fetch the actual contents of the Window
	hdc = GetDC(hWnd);
	hDest = CreateCompatibleDC(hdc);

	// create a compatible bitmap to store the pixels
	hBitmap = CreateCompatibleBitmap(hdc, Rect.right, Rect.bottom);

	// bits copied to the context will go to the bitmap
	hPrevBitmap = (HBITMAP) SelectObject(hDest, hBitmap);

	//copy from the window's dc to the dc with the bitmap
	BitBlt(hDest, 0, 0, Rect.right, Rect.bottom, hdc, 0, 0, SRCCOPY);
	// MUST unselect hBitmap in order to use GetDIBits() later
	// bssides, restoring previous object is good practice
	SelectObject(hDest, hPrevBitmap);

	// First call with buffer pointer null just retrieves
	// bitmap information into bi structure
    bi.bmiHeader.biBitCount = 0;  // must set to 0 to retrieve infoheader
	GetDIBits(hDest, hBitmap, 0, 0, NULL, &bi, DIB_RGB_COLORS);
	// Modify to the format we want to retrieve:
	// uncompressed, 3-byte RGB format
    bi.bmiHeader.biBitCount = 24;    
    bi.bmiHeader.biCompression = BI_RGB;    

	// Allocate memory for one row of the picture.
	// Format is 3 bytes (24 bits) per pixel.
	// The buffer is filled by GetDIBits().
	// The buffer is output to a file by libpng calls.
	// For sake of GetDIBits(), must round up to whole number of DWORDs (the "stride")
	row = (png_bytep) malloc((Rect.right * bi.bmiHeader.biBitCount / 8 * sizeof(png_byte) + 3) & ~3);
	if (row == NULL) {
		goto finalise;

	// Write image data one row at a time
	// Microsoft image is bottom-up, png is top-down, so must go
	// in reverse order for y.
	// Each Microsoft pixel is BGR, must swap bytes for RGB orderof png
	int x, y;
	UCHAR temp;
	for (y=Rect.bottom-1 ; y>=0; y--) {
		GetDIBits(hDest, hBitmap, y, 1, row, &bi, DIB_RGB_COLORS);
		for (x=0 ; x<Rect.right; x++) {
			temp = row[x*3];
			row[x*3] = row[x*3+2];
			//row[x*3+1] = row[x*3+1];  // middle byte unchanged
			row[x*3+2] = temp;
		png_write_row(png_ptr, row);

	// End write
	png_write_end(png_ptr, NULL);

	if (fp != NULL) fclose(fp);
	if (info_ptr != NULL) png_free_data(png_ptr, info_ptr, PNG_FREE_ALL, -1);
	if (png_ptr != NULL) png_destroy_write_struct(&png_ptr, (png_infopp)NULL);
	if (row != NULL) free(row);
	if (hdc != NULL) ReleaseDC(NULL, hdc);
	if (hDest != NULL) DeleteDC(hDest);
	if (hBitmap != NULL) DeleteObject(hBitmap);

	return code;

Leave a Reply