Loading GIF, JPG, BMP, WMF files and viewing them

UPDATE March 15, 2003:
I recently had a need to convert BMP files that had already been read into memory as-is, to bitmap handles for use by the Win32 GDI functions. I used the techniques described below, plus a IPicture method "GPget_Handle()". Code and description in this file: bitmap_handle_from_memory.txt.

I have also recently discovered a webpage that explains low-level details about COM, that I have not found anywhere else. The page describes how to create a HTML control and embed it in a window. Code is in C. URL: http://www.codeguru.com/ieprogram/cwebpage.html. The tutorial part on its own is excellent.

When I was developing the early beta versions of my new Windows application, EVE (see http://goosee.com), a vector graphics drawing program, it needed to be able to load and insert bitmap graphics images. That is, I wanted to load a graphic file and display it in my application's window.

Now, there are libraries available to do this, some of them free. They don't have source code, except for one, called GIFLIB, with MASM source code -- see a link to it from Hutch's MASM site. GIFLIB only loads GIF files.
I used it for the first release of my EVE product, however it is a bit buggy and crashes on some GIFs -- the LZW decompressor is the part that crashes.

After much hunting around, I found a technical article on Microsoft's web site, describing a program called LOADPIC -- you should be able to locate it from "LOADPIC" as a keyword. This is a C++ program that uses the OleLoadPicture() function. This function is very high-level and will convert GIF, JPG, BMP and WMF files into a format that can be displayed, including the decompressing.

This function is not described in Microsoft's SDKs prior to 1998, though the docs state that it works on Win95 -- I haven't tested my program on a wide range of Win95 systems -- the function is in OLEPRO32.DLL and it may not be in older Win95 systems, though I suspect that most will have it, as it is likely that a later application will have loaded it when it got installed.

The problem is, I knew nothing about OLE (Object Linking and Embedding) and COM (Common Object Model). For MASM programmers, there's a whole lot of stuff on this topic at Hutch's asm site (or was that Iczelion's site? -- whatever), submitted by Ernie Murphy. Also, this COM stuff is at Ernie's own site: http://here.is/COMinASM.

Ok, to tackle this, I took the LOADPIC.CCP application and compiled it using the free Borland C++ compiler -- it gave lots of compile errors, because the code was designed for Microsoft's Visual C++. But, I had set a switch on the compiler to generate an asm file, and at least the compiler got as far as that.
Armed with the asm file, I was able to figure out the code required for my program, however, the key function, OleLoadPicture(), returned an error code. So, I emailed my code to Ernie for his perusal. This greatly intrigued Ernie, as he had no idea that there was such a function to load graphics images (as is the case for just about every other Win32 programmer). Ernie has converted LOADPIC.CCP to LOADPIC.ASM and it is now available at his site.

Ernie's approach is very professional, and you have to include lots of macros, include files and link in various library files. I successfully assembled his example program, but I decided to have another look at my original "grassroots" solution. I discovered the bug that was preventing OleLoadPicture() to work, so I have placed my code below. The only extra libraries that you have to link to are OLEPRO32.LIB and OLE32.LIB -- you can find these from various places.

You'll need these two prototypes:


I'm talking MASM code here, but you should be able to translate to C or whatever.

I'll leave it up to you to write the code to open a file and load it into a memory block. Note that Ernie's example has the code for that. Let's say we have loaded the file and have variable "pmem" pointing to the memory block...

Here is my grassroots code for converting the file into a form that can be displayed:

IID_IPicture DWORD 07bf80980h ;unsigned long.
             WORD 0bf32h      ;unsigned short.
             WORD 0101ah      ;unsigned short
             BYTE 08bh,0bbh,0,0aah,0,030h,0ch,0abh ;unsigned char Data4[8];

gpPicture DWORD 0
E_POINTER EQU 080000005h

creategifhdc PROC STDCALL USES ebx ecx edx esi edi, \
  LOCAL hglobal:DWORD,pstm:DWORD,hr:DWORD
  LOCAL widthpixels:DWORD,heightpixels:DWORD
  LOCAL hmwidth:DWORD,hmheight:DWORD
;ret eax=0 ok, else error#.
;we enter func with pmem->bitmap-file-in-memory.
  mov gpPicture,0

For the OLE/COM functions to use it, the memory block has to be converted into something called a "stream" -- don't ask me what the difference is ...

;now need to convert the mem-block into a "stream"...
  invoke GlobalHandle,pmem
  mov hglobal,eax
  invoke CreateStreamOnHGlobal,hglobal,TRUE,ADDR pstm
  ;...returns pstm =ptr to stream (i.e. copy of the mem block).
  ;returns eax=0 if ok
  .IF eax != 0
    mov eax,220
    jmp er0

When I created the memory block, I saved its size, which I'm retrieving here, and calling the magical function OleLoadPicture(). IID_IPicture is a structure that defines what type of information is to be loaded, in this case a graphic image. This function will place a pointer into variable gpPicture, that points to an "interface" -- this is basically a structure, with addresses of "methods" (functions) that can manipulate the data...

  mov eax,mygifinfo.dwGIFDataSize ;size of mem block.
  invoke OleLoadPicture,pstm,eax,0,ADDR IID_IPicture,ADDR gpPicture
  ;..."IID_IPicture" is the type of interface pointer to return.
  ;...The requested interface pointer is returned in "gpPicture".
  ;returns eax=0 if ok
  .IF eax != 0 ;S_OK=0
    .IF eax==E_POINTER
      mov eax,222
      mov eax,223
      mov eax,224
      .IF SDWORD PTR eax<0
        mov eax,221 ;E_UNEXPECTED
        jmp ok9
    jmp er0

I don't need the "stream" anymore. Also, it wasn't in the original LOADPIC code, but I presumed that I also needed to free the original memory block (does releasing the stream also delete the original memory block? -- I have no idea). Release() is a method, and has to be accessed in this manner (see Ernie's code for a more formal way of doing this)...

  mov eax,pstm            ;pstm->Release();
  push eax                ;/
  mov edx,dword ptr [eax] ;/
  call dword ptr [edx+8]  ;/
  ;umm, presume can release original mem block?...
  invoke GlobalFree,hglobal

I need to call some more methods to get the width and height of the graphic, but they are returned in "himetric" units. I have converted these to pixels. Earlier in my program, in the handling of the WM_CREATE message, I had calculated the variables xscreenpixels and yscreenpixels, using this code:
          ;find out the resolution of the display...
          invoke GetDC, g_hwnd ;-->eax=display context client area.
          mov hdc1,eax
          invoke GetDeviceCaps, hdc1,LOGPIXELSX ;query pixels/inch.
          mov xscreenpixels,eax
          invoke GetDeviceCaps, hdc1,LOGPIXELSY ;query pixels/inch.
          mov yscreenpixels,eax
          invoke ReleaseDC, g_hwnd,hdc1

So, I can convert the himetric units to pixels... 

;the picture is now ready to be painted... gpPicture points (indirectly) to it...
  lea eax,hmwidth             ;gpPicture->get_Width(&hmWidth)
  push eax                    ;/
  mov edx,dword ptr gpPicture ;/
  push edx                    ;/
  mov ecx,dword ptr [edx]     ;/ (hmwidth is in himetric units)
  call dword ptr [ecx+24]     ;/
  lea eax,hmheight            ;gpPicture->get_Height(&hmHeight)
  push eax                    ;/
  mov edx,dword ptr gpPicture ;/
  push edx                    ;/
  mov ecx,dword ptr [edx]     ;/
  call dword ptr [ecx+28]     ;/ (hmheight is in himetric units)
  ;the wwidth and height are in HIMETRIC units. for output to screen, need
  ;the width/height in pixels...
  mov eax,hmwidth
  mul xscreenpixels
  mov ecx,2540
  div ecx
  mov widthpixels,eax
  mov eax,hmheight
  mul yscreenpixels
  mov ecx,2540
  div ecx
  mov heightpixels,eax

This section of code below is just for my own EVE program. I have an element that is to display on the screen and it has to hold gpPicture, the coordinates of the element on the diagram, and width/height in both pixels and himetric units. When EVE processes the WM_PAINT message, this info is retrieved from the element...

;the picture is going to be painted in common_paint_bitmap(), and need to
;setup the icon now and pass stuff...
  mov eax,gpPicture
  mov ebx,p1stdataelement
  mov (OBJECT PTR [ebx]).ximage,eax
  ;also, save width/height of bitmap into icon...
  ;and update xmid/ymid...
  mov edi,pobj
  mov eax,widthpixels
  mov (OBJECT PTR [edi]).wwidth,eax
  shr eax,1
  add eax,(OBJECT PTR [edi]).xtopleft
  mov (OBJECT PTR [edi]).xmid,eax
  mov eax,heightpixels
  mov (OBJECT PTR [edi]).height,eax
  shr eax,1
  add eax,(OBJECT PTR [edi]).ytopleft
  mov (OBJECT PTR [edi]).ymid,eax
  ;also need to save wwidth/height of image in 1st data-element...
  mov eax,(OBJECT PTR [edi]).wwidth
  mov WORD PTR (OBJECT PTR [ebx]).nshape,ax ;(alias for rplock2).
  mov eax,(OBJECT PTR [edi]).height
  mov WORD PTR (OBJECT PTR [ebx]).nshape+2,ax
  ;save the width/height in himetric units in the icon...
  mov eax,hmwidth
  mov (OBJECT PTR [edi]).widthhimetric,eax
  mov eax,hmheight
  mov (OBJECT PTR [edi]).heighthimetric,eax

  xor eax,eax ;ok.
er1: mov eax,219 ;error.
er0: ret
creategifhdc ENDP

Ok, so we've got this gpPicture pointer and the width/height of the image, in both himetric units and pixels. When the application processes the WM_PAINT message, you can have this code:

;width/height in pixels...
  mov esi,(OBJECT PTR [edi]).wwidth
  mov widthpixels,esi
  mov ebx,(OBJECT PTR [edi]).height
  mov heightpixels,ebx
;width/height in himetric units...
  mov esi,(OBJECT PTR [edi]).widthhimetric
  mov widthhimetric,esi
  mov ebx,(OBJECT PTR [edi]).heighthimetric
  mov heighthimetric,ebx
  mov ebx,p1stdataelement
  mov eax,(OBJECT PTR [ebx]).ximage ;gpPicture ;hDCBitmap
  mov ecx,(OBJECT PTR [edi]).xtopleft
  mov edx,(OBJECT PTR [edi]).ytopleft

; // display picture using IPicture::Render
; gpPicture->Render(hdc, 0, 0, nWidth, nHeight, 0, hmHeight, hmWidth, -hmHeight, &rc);
  ; lea ebx,clientrect
  ; push ebx ;0 ;really need addr of client rect here. ? (for wmf file)
  push 0
  mov ebx,heighthimetric  ;needs to be -ve, otherwise picture
  neg ebx                 ;displays upside down. see also below topcorner.
  push ebx                ;heighthimetric ;y height to copy from source picture. himetric
  push widthhimetric      ;x width to copy from source picture. units.
  push heighthimetric     ;upper-left corner of source, NOTE, not 0.
  push 0                  ; / himetric units.
  push heightpixels       ;height diagram coords.
  push widthpixels        ;wwidth /
  push edx             ;0 ;y upper-left corner,
  push ecx             ;0 ;x diagram coords.
  push hdc                ;hdc.
  ; mov eax,dword ptr gpPicture
  push eax
  mov edx,dword ptr [eax]
  call dword ptr [edx+32]

Ok, so as my program draws each element to the screen, it gets the gpPicture variable and the width/height parameters out. Then we call a method of the IPicture interface, called Render().

The basic structure of the code described above allows more than one picture to be simultaneously loaded and displayed. However, gpPicture points to an "interface" that points to methods (functions) as well as the actual bitmap data. So, when finished it is necessary to release the interface. My EVE program flushes these gpPicture's whenever a diagram is saved, as well as when the EVE program is exited. Here is the code to release a gpPicture interface:

mov eax,(OBJECT PTR [esi]).ximage
.IF eax != 0
  mov edx,eax       ;gpPicture ;gpPicture->Release();
  push edx                     ;/ "this" has to be passed, which is a ptr
  mov ecx,dword ptr [edx]      ;/ to the interface.
  call dword ptr [ecx+8]       ;/
  mov (OBJECT PTR [esi]).ximage,0

What EVE does is look through the elements in the database, and the .ximage parameter has gpPicture if the entry is non-zero. I then call the Release() method. Simple.

There you are, it works real nice. Have a look at my EVE site: http://goosee.com/ .

Barry Kauler