Introduction
Recently, I needed to display some JPG and PNG files. I had an old copy of
LeadTools and the open source libraries for both formats, but wanted my
executable to be as small as possible. So I decided to give GDI+ a try. I
quickly found GDI+ to be poorly designed and very quirky, but it worked well
for my purposes until I discovered, to my horror, that GDI+ cannot load JPG or
PNG images stored as resources!
Like, I'm sure, other developers facing this issue, I disbelieved the
documentation and tried Bitmap::FromResource
to no avail. While
perusing the Bitmap methods available, I ran across Bitmap::FromStream
.
After a bit of testing and several errors, due mostly to the horrible GDI+
documentation, I came up with working code. After a night of rest, I decided to
encapsulate the code in a simple class to ensure memory got freed. The result
were two classes: CGdiPlusBitmap
and CGdiPlusBitmapResource
.
The Gotcha
Before discussing the code itself, there is a caveat with GDI+ that must be
addressed. With JPG, some TIFF and other formats, the original image
information must be available at all times. In other words, if you open a
bitmap using Bitmap::FromFile
, you cannot delete or otherwise
change that file while the image is open. This same restriction applies to CGdiPlusBitmapResource
.
(My testing found that PNG and BMP files don't seem to follow this
generalization, though I don't know if this is standard behavior or just a
fluke with my file set.)
GDI+ Initialization
GDI+ needs to be initialized before any GDI+ calls are made. I suggest adding
the following data member to the class derived from CWinApp
:
ULONG_PTR m_gdiplusToken;
In InitInstance()
, add the following calls:
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, NULL);
In ExitInstance()
, add the following:
Gdiplus::GdiplusShutdown(m_gdiplusToken);
The Class
I created two classes, with the base class being a very simple encapsulation of
Bitmap
and the derived class encapsulating the global memory. I
suppose that if I ever had the patience and desire, I could extend that class,
but I have no need to do so. (If you're curious why I didn't simply derive from
the ATL class CImage
, it's because the code was used in a
program that didn't use MFC or ATL. However, the code is so simple, it could
easily be modified to use CImage
as the base class.)
I'm not going to bother going over the CGdiPlusBitmap
class except
to say that it has a single, public, data member Bitmap* m_pBitmap
.
(In the class I prefaced the GDI+ objects with the Gdiplus
namespace
in case the developer doesn't want to declare using namespace Gdiplus;
.)
The CGdiPlusBitmapResource
class has several constructors and
several overloaded Load
functions. The overloaded functions simply
allow lazy programmers, like myself, to not have to type MAKEINTRESOURCE
.
The main Load
function takes the resource name and type as strings
and is the key to the class. This code follows in its entirety:
inline
bool CGdiPlusBitmapResource::Load(LPCTSTR pName, LPCTSTR pType,
HMODULE hInst)
{
Empty();
HRSRC hResource = ::FindResource(hInst, pName, pType);
if (!hResource)
return false;
DWORD imageSize = ::SizeofResource(hInst, hResource);
if (!imageSize)
return false;
const void* pResourceData = ::LockResource(::LoadResource(hInst,
hResource));
if (!pResourceData)
return false;
m_hBuffer = ::GlobalAlloc(GMEM_MOVEABLE, imageSize);
if (m_hBuffer)
{
void* pBuffer = ::GlobalLock(m_hBuffer);
if (pBuffer)
{
CopyMemory(pBuffer, pResourceData, imageSize);
IStream* pStream = NULL;
if (::CreateStreamOnHGlobal(m_hBuffer, FALSE, &pStream) == S_OK)
{
m_pBitmap = Gdiplus::Bitmap::FromStream(pStream);
pStream->Release();
if (m_pBitmap)
{
if (m_pBitmap->GetLastStatus() == Gdiplus::Ok)
return true;
delete m_pBitmap;
m_pBitmap = NULL;
}
}
m_pBitmap = NULL;
::GlobalUnlock(m_hBuffer);
}
::GlobalFree(m_hBuffer);
m_hBuffer = NULL;
}
return false;
}
I find the code very self explanatory, though those that know the return value
of ::LoadResource
is an HGLOBAL
may find the apparent
double copy using CopyMemory
confusing. In brief CreateStreamOnHGlobal
requires a HGLOBAL
handle allocated by GlobalAlloc
using
the GMEM_MOVEABLE
flag.
The Demo
The demo is a Visual Studio .NET 2003 project with ANSI and UNICODE builds. It
allows you to load resampled JPG or PNG resources (For the curious, I took both
photographs in Oahu, Hawaii for, and while filming content of, a multimedia
product. One is of Laie Bay, the other is a sunset viewed from Waikiki.)
GDI+ Disclaimer
I am not a GDI+ expert, nor am I a big fan, even if I do find GDI+ occasionally
very useful. Please don't ask me questions about it.
Why not IPicture?
I've been asked why I didn't use IPicture
. The answer is threefold;
first, IPicture
does not support PNG images. Second, IPicture
is pretty much only an image loader, with little more capability than the
standard GDI bitmap calls. Third, IPicture
decodes the image data
immediately. JPG and GIF images will use more memory than this class.
Updates
22 April 2004
CreateStreamOnHGlobal
now uses FALSE
for the second
argument since Bitmap
requires that the memory be retained for at least JPG
images and I decided to err on the side of caution. Interestingly, my testing
showed this flag is often ignored, but I received reports that this wasn't
always the case and was technically incorrect.
In addition, while fixing this bug I realized I wasn't clearing up the global
memory on failure. That resulted in the code being rearranged as it is now.
15 June 2004
If Gdiplus::Bitmap::FromStream
fails, I added what I believe is
redundant, but more correct code in handling m_pBitmap
.
(Unfortunately, the documentation is silent on the subject as to whether NULL
will always be returned on failure.)
3 September 2004
Added a section on GDI+ initialization.
Fixed a potential memory leak on image load failure in the sample application.