Click here to Skip to main content
15,887,683 members
Articles / Multimedia / GDI+
Article

Loading JPG & PNG resources using GDI+

Rate me:
Please Sign up or sign in to vote.
4.84/5 (76 votes)
6 Sep 2004CPOL4 min read 873.9K   20.2K   157   164
A class to facilitate loading JPG and PNG files from resources using GDI+

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Joe is one of those software engineers with a film degree. His first paid programming job (you think film is a good way to make a living?) was writing games for Apple II's using 6502 assembly. He soon moved to 80x86 assembly, C, C++ (for a long time), C# and then back to C++ with occasional dabbling in C#, Python and other vile languages.

He first wrote software for Windows 3.0 in 1990. Save for some work in Linux, DOS and a mercifully brief foray into OS/2, he has concentrated on designing and writing software for all versions and types of Windows except RT.

Comments and Discussions

 
GeneralPNG 8bit 16bit Pin
Ray Guan16-Apr-09 23:37
Ray Guan16-Apr-09 23:37 
GeneralRe: PNG 8bit 16bit Pin
Joe Woodbury17-Apr-09 5:52
professionalJoe Woodbury17-Apr-09 5:52 
GeneralNote: CImage does not support transparent (alpha) PNG Pin
ZhenyOK14-Feb-09 23:28
ZhenyOK14-Feb-09 23:28 
GeneralSmall notice about image size ! Pin
Olivier Higelin7-Dec-08 23:42
Olivier Higelin7-Dec-08 23:42 
GeneralActive Double Click and move the button Pin
vic_mrh25-Nov-08 0:34
vic_mrh25-Nov-08 0:34 
GeneralRe: Active Double Click and move the button Pin
vic_mrh25-Nov-08 0:44
vic_mrh25-Nov-08 0:44 
Generallook like geneous code Pin
zhongyougang@gmail.com9-Oct-08 20:54
zhongyougang@gmail.com9-Oct-08 20:54 
NewsReplacing CreateStreamOnHGlobal with a custom IStream object to directly read resource data Pin
Nick_Acquaviva18-Aug-08 7:59
Nick_Acquaviva18-Aug-08 7:59 
Instead of having to copy the resource data to global memory in order to use CreateStreamOnHGlobal, I wrote a simple COM class to allow the image to be loaded directly from the resource data.

////////////////////////////////////////////////////////////////////////////////
//
class MEMORY_STREAM
//
////////////////////////////////////////////////////////////////////////////////
:
    IStream
{
    ULONG m_ReferenceCount;
    const PUCHAR m_Memory;
    ULARGE_INTEGER m_Offset;
    ULARGE_INTEGER m_Size;

public:
    MEMORY_STREAM
    (
        const void* Memory,
        DWORD Size
    );
    
    // IUnknown
    
    STDMETHODIMP QueryInterface
    (
        REFIID InterfaceId,
        void** Object
    );
    
    STDMETHODIMP_(ULONG) AddRef();
    
    STDMETHODIMP_(ULONG) Release(); 

    // ISequentialStream
        
    STDMETHODIMP Read
    ( 
        void* Buffer,
        ULONG ReadBytes,
        ULONG* BytesRead
    );
        
    STDMETHODIMP Write
    ( 
        const void* Buffer,
        ULONG WriteBytes,
        ULONG* BytesWritten
    );

    // IStream

    STDMETHODIMP Seek
    ( 
        LARGE_INTEGER Move,
        DWORD Origin,
        ULARGE_INTEGER* NewPosition
    );
    
    STDMETHODIMP SetSize
    ( 
        ULARGE_INTEGER NewSize
    );
    
    STDMETHODIMP CopyTo
    ( 
        IStream* Stream,
        ULARGE_INTEGER CopyBytes,
        ULARGE_INTEGER* BytesRead,
        ULARGE_INTEGER* BytesWritten
    );
    
    STDMETHODIMP Commit
    ( 
        DWORD Flags
    );
    
    STDMETHODIMP Revert();
    
    STDMETHODIMP LockRegion
    ( 
        ULARGE_INTEGER Offset,
        ULARGE_INTEGER Size,
        DWORD Type
    );
    
    STDMETHODIMP UnlockRegion
    ( 
        ULARGE_INTEGER Offset,
        ULARGE_INTEGER Size,
        DWORD Type
    );
    
    STDMETHODIMP Stat
    ( 
        STATSTG* Stats,
        DWORD Flag
    );
    
    STDMETHODIMP Clone
    ( 
        IStream** Stream
    );
};


////////////////////////////////////////////////////////////////////////////////
//
// MEMORY_STREAM Implementation
//
////////////////////////////////////////////////////////////////////////////////


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODCALLTYPE MEMORY_STREAM::MEMORY_STREAM
(
    const void* Image,
    DWORD Size
)
//
////////////////////////////////////////////////////////////////////////////////
:
    m_ReferenceCount(0),
    m_Memory((PUCHAR)Image)
{
    m_Offset.QuadPart = 0;
    m_Size.QuadPart = Size;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP_(ULONG) MEMORY_STREAM::AddRef()
//
////////////////////////////////////////////////////////////////////////////////
{
    return ++m_ReferenceCount;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP_(ULONG) MEMORY_STREAM::Release()
//
////////////////////////////////////////////////////////////////////////////////
{
    ULONG ReferenceCount(--m_ReferenceCount);
    
    if (m_ReferenceCount == 0)
    {
        delete this;
    }

    return ReferenceCount;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::QueryInterface
(
    REFIID InterfaceId,
    void** Object
)
//
////////////////////////////////////////////////////////////////////////////////
{
    while (true)
    {
        if (IsEqualGUID(InterfaceId, __uuidof(IUnknown)))
        {
            *Object = static_cast<IUnknown*>(this);
            break;
        }

        if (IsEqualGUID(InterfaceId, __uuidof(IStream)))
        {
            *Object = static_cast<IStream*>(this);
            break;
        }

        *Object = NULL;
        return E_NOINTERFACE;
    }

    AddRef();
    return S_OK;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::Read
( 
    void* Buffer,
    ULONG ReadBytes,
    ULONG* BytesRead
)
//
////////////////////////////////////////////////////////////////////////////////
{
    ULONG Length;
    HRESULT Result;

    if (m_Offset.QuadPart + ReadBytes > m_Size.QuadPart)
    {
        Length = (ULONG)(m_Size.QuadPart - m_Offset.QuadPart);
        Result = S_FALSE;
    }
    else
    {
        Length = ReadBytes;
        Result = S_OK;
    }
        
    CopyMemory(Buffer, m_Memory + (ULONG_PTR)m_Offset.QuadPart, Length);
    m_Offset.QuadPart += Length;

    if (BytesRead)
    {
        *BytesRead = Length;
    }

    return Result;
}

    
////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::Write
( 
    const void* Buffer,
    ULONG WriteBytes,
    ULONG* BytesWritten
)
//
////////////////////////////////////////////////////////////////////////////////
{
    return E_NOTIMPL;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::Seek
( 
    LARGE_INTEGER Move,
    DWORD Origin,
    ULARGE_INTEGER* NewPosition
)
//
////////////////////////////////////////////////////////////////////////////////
{
    switch (Origin)
    {
        case STREAM_SEEK_SET:
            m_Offset.QuadPart = Move.QuadPart;
            break;
            
        case STREAM_SEEK_CUR:
            if (m_Offset.QuadPart + Move.QuadPart > m_Size.QuadPart)
            {
                return STG_E_INVALIDFUNCTION;
            }
            
            m_Offset.QuadPart += Move.QuadPart;
            break;
            
        case STREAM_SEEK_END:
            if ((ULONGLONG)Move.QuadPart > m_Offset.QuadPart)
            {
                return STG_E_INVALIDFUNCTION;
            }
            
            m_Offset.QuadPart = m_Size.QuadPart - Move.QuadPart;
            break;
            
        default:
            return STG_E_INVALIDFUNCTION;
    }
            
    if (NewPosition)
    {
        *NewPosition = m_Offset;
    }

    return S_OK;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::SetSize
( 
    ULARGE_INTEGER NewSize
)
{
    return E_NOTIMPL;
}

////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::CopyTo
( 
    IStream* Stream,
    ULARGE_INTEGER CopyBytes,
    ULARGE_INTEGER* BytesRead,
    ULARGE_INTEGER* BytesWritten
)
//
////////////////////////////////////////////////////////////////////////////////
{
    return E_NOTIMPL;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::Commit
( 
    DWORD Flags
)
{
    return E_NOTIMPL;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::Revert()
{
    return E_NOTIMPL;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::LockRegion
( 
    ULARGE_INTEGER Offset,
    ULARGE_INTEGER Size,
    DWORD Type
)
//
////////////////////////////////////////////////////////////////////////////////
{
    return E_NOTIMPL;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::UnlockRegion
( 
    ULARGE_INTEGER Offset,
    ULARGE_INTEGER Size,
    DWORD Type
)
//
////////////////////////////////////////////////////////////////////////////////
{
    return E_NOTIMPL;
}


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::Stat
( 
    STATSTG* Stats,
    DWORD Flags
)
//
////////////////////////////////////////////////////////////////////////////////
{
    ZeroMemory(Stats, sizeof(*Stats));

    if ((Flags & STATFLAG_NONAME) != STATFLAG_NONAME)
    {
        Stats->pwcsName = (LPOLESTR)CoTaskMemAlloc(sizeof(OLECHAR));
        
        if (Stats->pwcsName)
        {
            *Stats->pwcsName = 0;
        }
    }
 
    Stats->type = STGTY_STREAM;
    Stats->cbSize = m_Size;
    return S_OK;
}        


////////////////////////////////////////////////////////////////////////////////
//
STDMETHODIMP MEMORY_STREAM::Clone
( 
    IStream** Stream
)
//
////////////////////////////////////////////////////////////////////////////////
{
    return E_NOTIMPL;
}


////////////////////////////////////////////////////////////////////////////////
//
HRESULT CreateStreamOnResource
(
    HMODULE Instance,
    PCTSTR Type,
    PCTSTR Name,
    IStream** Stream
)
//
////////////////////////////////////////////////////////////////////////////////
{
	HRSRC ResourceHandle = FindResource(Instance, Name, Type);

    if (ResourceHandle)
    {
    	DWORD Size = ::SizeofResource(Instance, ResourceHandle);

	    if (Size)
	    {
            HGLOBAL Resource = LoadResource(Instance, ResourceHandle);

            if (Resource)
            {
                const void* Data = LockResource(Resource);
                
                MEMORY_STREAM* MemoryStream = new MEMORY_STREAM(Data, Size);
                
                if (!MemoryStream)
                {
                    return E_OUTOFMEMORY;
                }
                
                return MemoryStream->QueryInterface
                (
                    __uuidof(IStream),
                    (void**)Stream
                );
            }
        }
    }

    return HRESULT_FROM_WIN32(GetLastError());
}


You can then use the class like this to create a GDI+ bitmap:

IStream* Stream;
Bitmap* bitmap(NULL);

if
(
    CreateStreamOnResource
    (
        Instance,
        _T("PNG"),
        MAKEINTRESOURCE(IDI_IMAGE),
        &Stream
    ) == S_OK
)
{
    bitmap = Gdiplus::Bitmap::FromStream(Stream);
    Stream->Release();
}


Nick Acquaviva
GeneralRe: Replacing CreateStreamOnHGlobal with a custom IStream object to directly read resource data Pin
Joe Woodbury18-Aug-08 9:27
professionalJoe Woodbury18-Aug-08 9:27 
GeneralRe: Replacing CreateStreamOnHGlobal with a custom IStream object to directly read resource data Pin
_Olivier_25-Mar-10 23:05
_Olivier_25-Mar-10 23:05 
GeneralRe: Replacing CreateStreamOnHGlobal with a custom IStream object to directly read resource data Pin
Nick_Acquaviva26-Mar-10 4:39
Nick_Acquaviva26-Mar-10 4:39 
General[Message Deleted] Pin
ellasun13-Aug-08 17:00
ellasun13-Aug-08 17:00 
GeneralRe: Bug in CreateStreamOnHGlobal Pin
Joe Woodbury14-Aug-08 5:21
professionalJoe Woodbury14-Aug-08 5:21 
GeneralRe: Bug in CreateStreamOnHGlobal Pin
ellasun17-Aug-08 20:24
ellasun17-Aug-08 20:24 
GeneralRe: Bug in CreateStreamOnHGlobal Pin
ellasun17-Aug-08 21:26
ellasun17-Aug-08 21:26 
Questioncould you port this to C# ? Pin
Bishoy Demian1-Jul-08 0:39
Bishoy Demian1-Jul-08 0:39 
AnswerRe: could you port this to C# ? Pin
Joe Woodbury1-Jul-08 5:19
professionalJoe Woodbury1-Jul-08 5:19 
GeneralThanks, I used this code here: Pin
Darren Sessions13-Jun-08 17:59
Darren Sessions13-Jun-08 17:59 
GeneralSame code in a single function Pin
Sean O'Connor29-Feb-08 5:11
Sean O'Connor29-Feb-08 5:11 
GeneralRe: Same code in a single function Pin
Joe Woodbury29-Feb-08 5:29
professionalJoe Woodbury29-Feb-08 5:29 
GeneralRe: Same code in a single function Pin
Sean O'Connor29-Feb-08 6:22
Sean O'Connor29-Feb-08 6:22 
GeneralRe: Same code in a single function Pin
Joe Woodbury29-Feb-08 9:28
professionalJoe Woodbury29-Feb-08 9:28 
GeneralThanks Joe! Pin
Gary Wheeler31-Jan-08 1:26
Gary Wheeler31-Jan-08 1:26 
QuestionGotcha???? Pin
User 5924119-Nov-07 17:14
User 5924119-Nov-07 17:14 
AnswerRe: Gotcha???? Pin
Joe Woodbury31-Jan-08 6:31
professionalJoe Woodbury31-Jan-08 6:31 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.