Save HBITMAP to *.bmp file using only Win32
I have a HBITMAP in my pure Win32 project (no external libraries are used). Can I export it to a *.bmp file using only Winapi and/or CRT functions so I don't have to add dependencies to the project?
Solution 1:
There is no API to save into file directly because, generally, having a bitmap handle does not mean you have direct access to bitmap data. Your solution is to copy bitmap into another bitmap with data access (DIB) and then using it data to write into file.
You typically either create another bitmap using CreateDIBSection
, or you get bitmap data with GetDIBits
.
CreateFile
, WriteFile
writes data into file.
You write: BITMAPFILEHEADER
, then BITMAPINFOHEADER
, then palette (which you typically don't have when bits/pixel is over 8), then data itself.
See also:
- C++: Hbitmap/BITMAP into .bmp file (this answer)
- Saving .bmp file using hBitmap = CreateDIBSection() in C Win32
- Storing an Image on MSDN
The Code
This is the code from the MSDN article (Note that you need to define the errhandler()
function):
PBITMAPINFO CreateBitmapInfoStruct(HWND hwnd, HBITMAP hBmp)
{
BITMAP bmp;
PBITMAPINFO pbmi;
WORD cClrBits;
// Retrieve the bitmap color format, width, and height.
if (!GetObject(hBmp, sizeof(BITMAP), (LPSTR)&bmp))
errhandler("GetObject", hwnd);
// Convert the color format to a count of bits.
cClrBits = (WORD)(bmp.bmPlanes * bmp.bmBitsPixel);
if (cClrBits == 1)
cClrBits = 1;
else if (cClrBits <= 4)
cClrBits = 4;
else if (cClrBits <= 8)
cClrBits = 8;
else if (cClrBits <= 16)
cClrBits = 16;
else if (cClrBits <= 24)
cClrBits = 24;
else cClrBits = 32;
// Allocate memory for the BITMAPINFO structure. (This structure
// contains a BITMAPINFOHEADER structure and an array of RGBQUAD
// data structures.)
if (cClrBits < 24)
pbmi = (PBITMAPINFO) LocalAlloc(LPTR,
sizeof(BITMAPINFOHEADER) +
sizeof(RGBQUAD) * (1<< cClrBits));
// There is no RGBQUAD array for these formats: 24-bit-per-pixel or 32-bit-per-pixel
else
pbmi = (PBITMAPINFO) LocalAlloc(LPTR,
sizeof(BITMAPINFOHEADER));
// Initialize the fields in the BITMAPINFO structure.
pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
pbmi->bmiHeader.biWidth = bmp.bmWidth;
pbmi->bmiHeader.biHeight = bmp.bmHeight;
pbmi->bmiHeader.biPlanes = bmp.bmPlanes;
pbmi->bmiHeader.biBitCount = bmp.bmBitsPixel;
if (cClrBits < 24)
pbmi->bmiHeader.biClrUsed = (1<<cClrBits);
// If the bitmap is not compressed, set the BI_RGB flag.
pbmi->bmiHeader.biCompression = BI_RGB;
// Compute the number of bytes in the array of color
// indices and store the result in biSizeImage.
// The width must be DWORD aligned unless the bitmap is RLE
// compressed.
pbmi->bmiHeader.biSizeImage = ((pbmi->bmiHeader.biWidth * cClrBits +31) & ~31) /8
* pbmi->bmiHeader.biHeight;
// Set biClrImportant to 0, indicating that all of the
// device colors are important.
pbmi->bmiHeader.biClrImportant = 0;
return pbmi;
}
void CreateBMPFile(HWND hwnd, LPTSTR pszFile, PBITMAPINFO pbi,
HBITMAP hBMP, HDC hDC)
{
HANDLE hf; // file handle
BITMAPFILEHEADER hdr; // bitmap file-header
PBITMAPINFOHEADER pbih; // bitmap info-header
LPBYTE lpBits; // memory pointer
DWORD dwTotal; // total count of bytes
DWORD cb; // incremental count of bytes
BYTE *hp; // byte pointer
DWORD dwTmp;
pbih = (PBITMAPINFOHEADER) pbi;
lpBits = (LPBYTE) GlobalAlloc(GMEM_FIXED, pbih->biSizeImage);
if (!lpBits)
errhandler("GlobalAlloc", hwnd);
// Retrieve the color table (RGBQUAD array) and the bits
// (array of palette indices) from the DIB.
if (!GetDIBits(hDC, hBMP, 0, (WORD) pbih->biHeight, lpBits, pbi,
DIB_RGB_COLORS))
{
errhandler("GetDIBits", hwnd);
}
// Create the .BMP file.
hf = CreateFile(pszFile,
GENERIC_READ | GENERIC_WRITE,
(DWORD) 0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
(HANDLE) NULL);
if (hf == INVALID_HANDLE_VALUE)
errhandler("CreateFile", hwnd);
hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M"
// Compute the size of the entire file.
hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) +
pbih->biSize + pbih->biClrUsed
* sizeof(RGBQUAD) + pbih->biSizeImage);
hdr.bfReserved1 = 0;
hdr.bfReserved2 = 0;
// Compute the offset to the array of color indices.
hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) +
pbih->biSize + pbih->biClrUsed
* sizeof (RGBQUAD);
// Copy the BITMAPFILEHEADER into the .BMP file.
if (!WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER),
(LPDWORD) &dwTmp, NULL))
{
errhandler("WriteFile", hwnd);
}
// Copy the BITMAPINFOHEADER and RGBQUAD array into the file.
if (!WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER)
+ pbih->biClrUsed * sizeof (RGBQUAD),
(LPDWORD) &dwTmp, ( NULL)))
errhandler("WriteFile", hwnd);
// Copy the array of color indices into the .BMP file.
dwTotal = cb = pbih->biSizeImage;
hp = lpBits;
if (!WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp,NULL))
errhandler("WriteFile", hwnd);
// Close the .BMP file.
if (!CloseHandle(hf))
errhandler("CloseHandle", hwnd);
// Free memory.
GlobalFree((HGLOBAL)lpBits);
}
Solution 2:
i'll leave this self contained proof of concept here since I'll probably need to look it up later since it's not obvious. It takes a screenshot of the desktop window and saves it into bitmap.bmp:
#include <Windows.h>
#include <stdio.h>
#include <assert.h>
/* forward declarations */
PBITMAPINFO CreateBitmapInfoStruct(HBITMAP);
void CreateBMPFile(LPTSTR pszFile, HBITMAP hBMP);
int main(int argc, char **argv);
PBITMAPINFO CreateBitmapInfoStruct(HBITMAP hBmp)
{
BITMAP bmp;
PBITMAPINFO pbmi;
WORD cClrBits;
// Retrieve the bitmap color format, width, and height.
assert(GetObject(hBmp, sizeof(BITMAP), (LPSTR)&bmp));
// Convert the color format to a count of bits.
cClrBits = (WORD)(bmp.bmPlanes * bmp.bmBitsPixel);
if (cClrBits == 1)
cClrBits = 1;
else if (cClrBits <= 4)
cClrBits = 4;
else if (cClrBits <= 8)
cClrBits = 8;
else if (cClrBits <= 16)
cClrBits = 16;
else if (cClrBits <= 24)
cClrBits = 24;
else cClrBits = 32;
// Allocate memory for the BITMAPINFO structure. (This structure
// contains a BITMAPINFOHEADER structure and an array of RGBQUAD
// data structures.)
if (cClrBits < 24)
pbmi = (PBITMAPINFO) LocalAlloc(LPTR,
sizeof(BITMAPINFOHEADER) +
sizeof(RGBQUAD) * (1<< cClrBits));
// There is no RGBQUAD array for these formats: 24-bit-per-pixel or 32-bit-per-pixel
else
pbmi = (PBITMAPINFO) LocalAlloc(LPTR,
sizeof(BITMAPINFOHEADER));
// Initialize the fields in the BITMAPINFO structure.
pbmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
pbmi->bmiHeader.biWidth = bmp.bmWidth;
pbmi->bmiHeader.biHeight = bmp.bmHeight;
pbmi->bmiHeader.biPlanes = bmp.bmPlanes;
pbmi->bmiHeader.biBitCount = bmp.bmBitsPixel;
if (cClrBits < 24)
pbmi->bmiHeader.biClrUsed = (1<<cClrBits);
// If the bitmap is not compressed, set the BI_RGB flag.
pbmi->bmiHeader.biCompression = BI_RGB;
// Compute the number of bytes in the array of color
// indices and store the result in biSizeImage.
// The width must be DWORD aligned unless the bitmap is RLE
// compressed.
pbmi->bmiHeader.biSizeImage = ((pbmi->bmiHeader.biWidth * cClrBits +31) & ~31) /8
* pbmi->bmiHeader.biHeight;
// Set biClrImportant to 0, indicating that all of the
// device colors are important.
pbmi->bmiHeader.biClrImportant = 0;
return pbmi;
}
void CreateBMPFile(LPTSTR pszFile, HBITMAP hBMP)
{
HANDLE hf; // file handle
BITMAPFILEHEADER hdr; // bitmap file-header
PBITMAPINFOHEADER pbih; // bitmap info-header
LPBYTE lpBits; // memory pointer
DWORD dwTotal; // total count of bytes
DWORD cb; // incremental count of bytes
BYTE *hp; // byte pointer
DWORD dwTmp;
PBITMAPINFO pbi;
HDC hDC;
hDC = CreateCompatibleDC(GetWindowDC(GetDesktopWindow()));
SelectObject(hDC, hBMP);
pbi = CreateBitmapInfoStruct(hBMP);
pbih = (PBITMAPINFOHEADER) pbi;
lpBits = (LPBYTE) GlobalAlloc(GMEM_FIXED, pbih->biSizeImage);
assert(lpBits) ;
// Retrieve the color table (RGBQUAD array) and the bits
// (array of palette indices) from the DIB.
assert(GetDIBits(hDC, hBMP, 0, (WORD) pbih->biHeight, lpBits, pbi,
DIB_RGB_COLORS));
// Create the .BMP file.
hf = CreateFile(pszFile,
GENERIC_READ | GENERIC_WRITE,
(DWORD) 0,
NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
(HANDLE) NULL);
assert(hf != INVALID_HANDLE_VALUE) ;
hdr.bfType = 0x4d42; // 0x42 = "B" 0x4d = "M"
// Compute the size of the entire file.
hdr.bfSize = (DWORD) (sizeof(BITMAPFILEHEADER) +
pbih->biSize + pbih->biClrUsed
* sizeof(RGBQUAD) + pbih->biSizeImage);
hdr.bfReserved1 = 0;
hdr.bfReserved2 = 0;
// Compute the offset to the array of color indices.
hdr.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) +
pbih->biSize + pbih->biClrUsed
* sizeof (RGBQUAD);
// Copy the BITMAPFILEHEADER into the .BMP file.
assert(WriteFile(hf, (LPVOID) &hdr, sizeof(BITMAPFILEHEADER),
(LPDWORD) &dwTmp, NULL));
// Copy the BITMAPINFOHEADER and RGBQUAD array into the file.
assert(WriteFile(hf, (LPVOID) pbih, sizeof(BITMAPINFOHEADER)
+ pbih->biClrUsed * sizeof (RGBQUAD),
(LPDWORD) &dwTmp, ( NULL)));
// Copy the array of color indices into the .BMP file.
dwTotal = cb = pbih->biSizeImage;
hp = lpBits;
assert(WriteFile(hf, (LPSTR) hp, (int) cb, (LPDWORD) &dwTmp,NULL));
// Close the .BMP file.
assert(CloseHandle(hf));
// Free memory.
GlobalFree((HGLOBAL)lpBits);
}
int main(int argc, char **argv)
{
HWND hwnd;
HDC hdc[2];
HBITMAP hbitmap;
RECT rect;
hwnd = GetDesktopWindow();
GetClientRect(hwnd, &rect);
hdc[0] = GetWindowDC(hwnd);
hbitmap = CreateCompatibleBitmap(hdc[0], rect.right, rect.bottom);
hdc[1] = CreateCompatibleDC(hdc[0]);
SelectObject(hdc[1], hbitmap);
BitBlt (
hdc[1],
0,
0,
rect.right,
rect.bottom,
hdc[0],
0,
0,
SRCCOPY
);
CreateBMPFile("bitmap.bmp", hbitmap);
return 0;
}
Solution 3:
Yes, this is possible, using the Windows Imaging Component (WIC). WIC offers built-in encoders, so that you don't have to manually write out bitmap headers and data. It also allows you to choose a different encoder (e.g. PNG), by changing as little as one line of code.
The process is fairly straight forward. It consists of the following steps:
- Retrieve properties from the source
HBITMAP
usingGetObject
(dimensions, bit depth). - Create an
IWICImagingFactory
instance. - Create an
IWICBitmap
instance from theHBITMAP
(IWICImagingFactory::CreateBitmapFromHBITMAP
). - Create an
IWICStream
instance (IWICImagingFactory::CreateStream
), and attach it to a filename (IWICStream::InitializeFromFilename
). - Create an
IWICBitmapEncoder
instance (IWICImagingFactory::CreateEncoder
), and associate it with the stream (IWICBitmapEncoder::Initialize
). - Create an
IWICBitmapFrameEncode
instance (IWICBitmapEncoder::CreateNewFrame
), and initialize it in compliance with the sourceHBITMAP
(IWICBitmapFrameEncode::Initialize
,IWICBitmapFrameEncode::SetSize
,IWICBitmapFrameEncode::SetPixelFormat
). - Write bitmap data to the frame (
IWICBitmapFrameEncode::WriteSource
). - Commit frame and data to stream (
IWICBitmapFrameEncode::Commit
,IWICBitmapEncoder::Commit
).
Translated to code:
#define COBJMACROS
#include <Objbase.h>
#include <wincodec.h>
#include <Windows.h>
#include <Winerror.h>
#pragma comment(lib, "Windowscodecs.lib")
HRESULT WriteBitmap(HBITMAP bitmap, const wchar_t* pathname) {
HRESULT hr = S_OK;
// (1) Retrieve properties from the source HBITMAP.
BITMAP bm_info = { 0 };
if (!GetObject(bitmap, sizeof(bm_info), &bm_info))
hr = E_FAIL;
// (2) Create an IWICImagingFactory instance.
IWICImagingFactory* factory = NULL;
if (SUCCEEDED(hr))
hr = CoCreateInstance(&CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER,
&IID_IWICImagingFactory, &factory);
// (3) Create an IWICBitmap instance from the HBITMAP.
IWICBitmap* wic_bitmap = NULL;
if (SUCCEEDED(hr))
hr = IWICImagingFactory_CreateBitmapFromHBITMAP(factory, bitmap, NULL,
WICBitmapIgnoreAlpha,
&wic_bitmap);
// (4) Create an IWICStream instance, and attach it to a filename.
IWICStream* stream = NULL;
if (SUCCEEDED(hr))
hr = IWICImagingFactory_CreateStream(factory, &stream);
if (SUCCEEDED(hr))
hr = IWICStream_InitializeFromFilename(stream, pathname, GENERIC_WRITE);
// (5) Create an IWICBitmapEncoder instance, and associate it with the stream.
IWICBitmapEncoder* encoder = NULL;
if (SUCCEEDED(hr))
hr = IWICImagingFactory_CreateEncoder(factory, &GUID_ContainerFormatBmp, NULL,
&encoder);
if (SUCCEEDED(hr))
hr = IWICBitmapEncoder_Initialize(encoder, (IStream*)stream,
WICBitmapEncoderNoCache);
// (6) Create an IWICBitmapFrameEncode instance, and initialize it
// in compliance with the source HBITMAP.
IWICBitmapFrameEncode* frame = NULL;
if (SUCCEEDED(hr))
hr = IWICBitmapEncoder_CreateNewFrame(encoder, &frame, NULL);
if (SUCCEEDED(hr))
hr = IWICBitmapFrameEncode_Initialize(frame, NULL);
if (SUCCEEDED(hr))
hr = IWICBitmapFrameEncode_SetSize(frame, bm_info.bmWidth, bm_info.bmHeight);
if (SUCCEEDED(hr)) {
GUID pixel_format = GUID_WICPixelFormat24bppBGR;
hr = IWICBitmapFrameEncode_SetPixelFormat(frame, &pixel_format);
}
// (7) Write bitmap data to the frame.
if (SUCCEEDED(hr))
hr = IWICBitmapFrameEncode_WriteSource(frame, (IWICBitmapSource*)wic_bitmap,
NULL);
// (8) Commit frame and data to stream.
if (SUCCEEDED(hr))
hr = IWICBitmapFrameEncode_Commit(frame);
if (SUCCEEDED(hr))
hr = IWICBitmapEncoder_Commit(encoder);
// Cleanup
if (frame)
IWICBitmapFrameEncode_Release(frame);
if (encoder)
IWICBitmapEncoder_Release(encoder);
if (stream)
IWICStream_Release(stream);
if (wic_bitmap)
IWICBitmap_Release(wic_bitmap);
if (factory)
IWICImagingFactory_Release(factory);
return hr;
}
Here's a companion test application to showcase the usage. Make sure to #define OEMRESOURCE
prior to including any system headers to allow use of the OBM_
images.
int wmain(int argc, wchar_t** argv) {
HRESULT hr = S_OK;
hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
if (FAILED(hr))
return -1;
HBITMAP bitmap = LoadImage(NULL, MAKEINTRESOURCE(OBM_ZOOM), IMAGE_BITMAP, 0, 0,
LR_DEFAULTCOLOR);
hr = WriteBitmap(bitmap, argv[1]);
// Cleanup
if (bitmap)
DeleteObject(bitmap);
CoUninitialize();
return 0;
}
This will load a system-provided bitmap, and save it to the pathname specified as an argument on the command line.
Limitations:
- No support for alpha channels. Although bitmaps version 5 support alpha channels, I am not aware of any way to find out, whether an
HBITMAP
refers to a bitmap with an alpha channel, nor would I know, how to determine, whether it is pre-multiplied. If you do want to support an alpha channel, make sure to set theEnableV5Header32bppBGRA
property toVARIANT_TRUE
(see BMP Format: Encoding). - No support for palletized bitmaps (bpp <= 8). If you are dealing with palletized bitmaps, make sure to supply an appropriate
HPALETTE
in the call toIWICImagingFactory::CreateBitmapFromHBITMAP
. - The encoder is initialized with the
GUID_WICPixelFormat24bppBGR
pixel format constant. A more versatile implementation would deduce a compatible pixel format from the sourceHBITMAP
.
Solution 4:
Yet another minimalistic option is to use OLE's IPicture
. It's always been around, still a part of Win32 API:
#define _S(exp) (([](HRESULT hr) { if (FAILED(hr)) _com_raise_error(hr); return hr; })(exp));
PICTDESC pictdesc = {};
pictdesc.cbSizeofstruct = sizeof(pictdesc);
pictdesc.picType = PICTYPE_BITMAP;
pictdesc.bmp.hbitmap = hBitmap;
CComPtr<IPicture> picture;
_S( OleCreatePictureIndirect(&pictdesc, __uuidof(IPicture), FALSE, (LPVOID*)&picture) );
// Save to a stream
CComPtr<IStream> stream;
_S( CreateStreamOnHGlobal(NULL, TRUE, &stream) );
LONG cbSize = 0;
_S( picture->SaveAsFile(stream, TRUE, &cbSize) );
// Or save to a file
CComPtr<IPictureDisp> disp;
_S( picture->QueryInterface(&disp) );
_S( OleSavePictureFile(disp, CComBSTR("C:\\Temp\\File.bmp")) );