Mosaic: Image types

The only image type we've implemented until now is the numbered tiles type. We will add a standard image type (bitmap from the resource file), and a user defined image type (where the user can select a bitmap to use on the tiles).

11.1 - Standard image

First add this to the resource file:

#define BMP_DEMOBITMAP 802
BMP_DEMOBITMAP BITMAP DISCARDABLE "resources\\demo.bmp"

and this to mosaic.inc:

BMP_DEMO BITMAP equ 802

This will add this bitmap to the resource file, with ID 802:

Demo image
demo.bmp, 200x200 pixels

Add a new handler to SetBitmap:

In SetBitmap, before .ENDIF:

.ELSEIF     eax==IMAGETYPE_STANDARD
    ;--- delete old image ---
    invoke  DeleteObject, hImage
    ;--- load bitmap from resource ---
    invoke  LoadBitmap, hInstance, ID_DEMOBITMAP
    ;--- save new handle ---
    mov     hImage, eax
    ;--- select new handle in ImageDC ---
    invoke  SelectObject, ImageDC, eax
    ;--- Create the 3D effect on the bitmap
    invoke  CreateTiles

The old image is deleted, the bitmap is loaded from the resources (LoadBitmap), the image handle is saved and selected in ImageDC and the 3D effect is drawn on it with CreateTiles. That's all there is, it should work now.

If you run the program now and select the standard image type you can see the image above on the tiles. The color schemes only affect the background color with this image type:

Image with green color schemeImage with red color schemeImage with blue color scheme

11.2 - User defined image

The standard image was easy to implement because it can be loaded from the resources and you know for sure that the image is valid. When the user can choose it's own bitmap it's more complicated, because of these reasons:

  • The image has to be loaded from harddisk, and there's no windows function that does that so we will have to do it by hand.
  • It should be fool proof. The program shouldn't crash if the user selects a textfile as bitmap.
  • The image size should be 200x200 pixels. We'll have to check the bitmap for that.

All the funtions in the following handlers will be defined below, this is just the mainframe of the process:

In SetBitmap, before .ENDIF:

    .ELSEIF eax==IMAGETYPE_BITMAP
        invoke  SelectObject, ImageDC, hImage
        invoke  CreateTiles

This adds the new image type so SetBitmap will respond to it. When SetBitmap is called with IMAGETYPE_BITMAP, SetBitmap assumes the bitmap is loaded in hImage. The only thing SetBitmap then does is select it in the ImageDC and make tiles of the bitmap.

In ProcessMenuItems, the MI_USEFILE handler, replace ';yet to do' with:

    mov     eax, offset OFN_file
    mov     al, byte ptr [eax]
    .IF     al==NULL
        invoke  GetOpenFileName, ADDR BitmapOFN
    .ENDIF
    .IF     eax!=NULL
        invoke  OpenBitmap, hWnd
        invoke  SetBitmap, hWnd, IMAGETYPE_BITMAP
        invoke  InvalidateRect, hWnd, NULL, FALSE
    .ENDIF

When the user clicks on the menu item with ID MI_USEFILE, the first byte of OFN_FILE is checked for zero. OFN_FILE will be defined later, but is just a buffer for the filename for the image. This zero-check is done to check if a filename has already been chosen. If not, GetOpenFileName (explained later) will show a 'Open file' dialog that fills a OPENFILENAME structure (BitmapOFN) and OFN_file. If this function returns a non-zero value, the user has chosen a file. OpenBitmap opens this file (and checks if it's valid). SetBitmap then changes the image type, and InvalidateRect updates the main window.
Note: Even if OpenBitmap failes, SetBitmap is still called. This has no effect because if OpenBitmap fails, it sets the image type to the standard image. SetBitmap justs redraws the standard image (useless but not harmfull).

In ProcessMenuItems, in the MI_OPENBITMAP handler:

.ELSEIF ax==MI_OPENBITMAP
    invoke  GetOpenFileName, ADDR BitmapOFN
    .IF     eax!=NULL
        invoke  ProcessMenuItems, hWnd, MI_USEFILE
    .ENDIF

The menu option 'Open Bitmap' in the file menu should also open a bitmap. If the user selects a file (eax!=null), ProcessMenuitems is called manually to set the file as image type.

Data used

.data
OFN_Filter          db      "Bitmaps (*.bmp)",0,"*.bmp",0,"All Files (*.*)",0,"*.*",0,0

BitmapOFN       OPENFILENAME    <SIZEOF OPENFILENAME,\
                                NULL,\
                                NULL,\
                                offset OFN_Filter,\
                                NULL, NULL, NULL, offset OFN_file,\
                                270,\
                                NULL, NULL, NULL, NULL,\
                                OFN_PATHMUSTEXIST + OFN_FILEMUSTEXIST,\
                                NULL, NULL, NULL, NULL, NULL, NULL>
.data?]
OFN_file        db      270 dup (?)

BitmapOFN is an OPENFILENAME structure. This structure is used to create a standard 'OpenFileName' dialog. The important members of this structure are:

offset OFN_Filter: This is a pointer to OFN_Filter. OFN_Filter is an array of string pairs, defining the file types the user can open. Both strings in each pair are terminated by a single 0 byte, the complete array is terminated by two 0 bytes. The first string in the pairs are the strings displayed in the 'file type' box, the second string is the search pattern for that file type.
offset OFN_file: A pointer to the buffer that receives the filename selected.
270: The maximum size of the buffer.
OFN_PATHMUSTEXISTS + OFN_FILEMUSTEXIST: Flags that ensure that the user has selected a valid path and filename.

This structure is enough for GetOpenFileName to show the dialog. It returns 0 if the user pressed cancel, and non-null if the user has chosen a filename.

OpenBitmap

OpenBitmap          PROTO STDCALL   :DWORD

;================================================================================
;                           OpenBitmap
;================================================================================
OpenBitmap      proc    uses edi esi    hWnd:DWORD
LOCAL   hFile:DWORD
LOCAL   FileSize:DWORD
LOCAL   hMem:DWORD
LOCAL   pMem:DWORD
LOCAL   BytesRead:DWORD

    invoke  DeleteObject, hImage
    invoke  CreateFile, ADDR OFN_file, GENERIC_READ, FILE_SHARE_READ,\
            NULL, OPEN_EXISTING, NULL, NULL
    .IF     eax==INVALID_HANDLE_VALUE
        MSGBOX  "Error: Could not open bitmap file."
        invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press
        ret
    .ENDIF
    mov     hFile, eax

    invoke  GetFileSize, hFile, NULL
    mov     FileSize, eax

    invoke  GlobalAlloc, GMEM_MOVEABLE, eax
    mov     hMem, eax
    invoke  GlobalLock, eax
    mov     pMem, eax

    invoke  ReadFile, hFile, pMem, FileSize, ADDR BytesRead, NULL

    mov     ecx, FileSize
    .IF     BytesRead!=ecx || eax==NULL
        MSGBOX  "Error: Failed reading from bitmap file."
        invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press
        jmp     @ob_exit1
    .ENDIF

    invoke  CheckIfValidBitmap, hWnd, pMem, FileSize
    .IF     eax==NULL
        MSGBOX  "ERROR: Bitmap is invalid."
        invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press
    .ELSEIF eax==1
        mov     ecx, pMem
        mov     edx, ecx
        mov     edx, dword ptr [edx + 0Ah]
        add     edx, ecx
        add     ecx, 0Eh ; Bitmapheader


        invoke  CreateDIBitmap, ImageDC, ecx, CBM_INIT,\
                    edx, ecx, DIB_RGB_COLORS
        mov     hImage, eax
    .ELSEIF     eax==2
            MSGBOX  "ERROR: This bitmap is not 200x200 pixels."
            mov     eax, offset OFN_file
            mov     byte ptr [eax], 0
            invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press
    .ENDIF
@ob_exit1:
    invoke  GlobalUnlock, hMem
    invoke  GlobalFree, hMem
    invoke  CloseHandle, hFile

ret
OpenBitmap      endp

After a valid filename has been selected, OpenBitmap loads the bitmap in memory and processes it. We will examine this function step by step:

Delete the old image:

    invoke  DeleteObject, hImage

Open the file in the OFN_file buffer. CreateFile (which in spite of it's name can open files too :) opens the file with readonly access (GENERIC_READ), and requires that the file actually exists to open it (OPEN_EXISTING). If it returns an invalid handle, an error message is shown with the MSGBOX macro (see mosaic.inc), and the image type is reset to the standard image type:

    invoke  CreateFile, ADDR OFN_file, GENERIC_READ, FILE_SHARE_READ,\
            NULL, OPEN_EXISTING, NULL, NULL
    .IF     eax==INVALID_HANDLE_VALUE
        MSGBOX  "Error: Could not open bitmap file."
        invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press
        ret
    .ENDIF
    mov     hFile, eax ;save handle

Retrieve filesize:

    invoke  GetFileSize, hFile, NULL
    mov     FileSize, eax

Create a free memory block with the size of the file. GlobalAlloc allocates a given number of bytes (eax contains the filesize) and returns a handle to the memory block (hMem). To get a pointer to the memory block, use GlobalLock. The pointer is savid in pMem.

    invoke  GlobalAlloc, GMEM_MOVEABLE, eax
    mov     hMem, eax
    invoke  GlobalLock, eax
    mov     pMem, eax

The complete bitmap is read with ReadFile. The destination is the value of pMem, which is the pointer to the allocated memory block. The number of bytes to read is FileSize, i.e. the complete file. The start offset to read can not be given as a parameter to ReadFile, it uses the current file pointer. But as we just created the file, the file pointer is reset to 0.

invoke  ReadFile, hFile, pMem, FileSize, ADDR BytesRead, NULL

If the number of bytes read is not the filesize, or if the function returns 0, the reading failed:

    mov     ecx, FileSize
    .IF     BytesRead!=ecx || eax==NULL
        MSGBOX  "Error: Failed reading from bitmap file."
        invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press
        jmp     @ob_exit1
    .ENDIF

This function will be defined later, but it returns 0 if the image is invalid, 2 if the image is not 200x200 pixels (see below), and 1 if the bitmap is valid.

    invoke  CheckIfValidBitmap, hWnd, pMem, FileSize
    .IF     eax==NULL
        MSGBOX  "ERROR: Bitmap is invalid."
        invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press

If the bitmap is valid:

   .ELSEIF eax==1

Here the dword at offset 0A (hex) in the file is read (This is the bmBits member of the BITMAP structure, which the BMP file format uses). This dword contains an offset to the actual image data. This offset is added to the memory pointer in ecx to get a pointer to this data.

        mov     ecx, pMem
        mov     edx, ecx
        mov     edx, dword ptr [edx + 0Ah]
        add     edx, ecx
        add     ecx, 0Eh ; Bitmapheader

CreateDIBitmap uses the pointer to the bitmapdata and the pointer to the start of the file to create a bitmap from it. The handle is saved.

       invoke  CreateDIBitmap, ImageDC, ecx, CBM_INIT,\
                    edx, ecx, DIB_RGB_COLORS
       mov     hImage, eax

If the size is incorrect:

    .ELSEIF     eax==2
            MSGBOX  "ERROR: This bitmap is not 200x200 pixels."
            mov     eax, offset OFN_file
            mov     byte ptr [eax], 0
            invoke  ProcessMenuItems, hWnd, MI_USESTANDARD  ;fake menu press
    .ENDIF

Free the allocated memory and close the filehandle:

@ob_exit1:
    invoke  GlobalUnlock, hMem
    invoke  GlobalFree, hMem
    invoke  CloseHandle, hFile

CheckIfValidBitmap

We can use OpenBitmap without CheckIfValidBitmap, but then it would not be fool proof. CheckIfValidBitmap uses some information of the bitmap file format to check if the file is a bitmap and has the right size.

CheckIfValidBitmap  PROTO STDCALL   :DWORD, :DWORD, :DWORD

;================================================================================
;                           Check If valid bitmap
;================================================================================
CheckIfValidBitmap proc uses edi hWnd:DWORD, pMem:DWORD, FileSize:DWORD
     mov    edi, pMem
     mov    ax, word ptr [edi]
     .IF    ax!="MB"
        xor     eax, eax
        ret
     .ENDIF

     mov    eax, dword ptr [edi+2]
     .IF    eax!=FileSize
        xor     eax, eax
        ret
     .ENDIF

     mov    eax, dword ptr [edi+0Eh]
     .IF    eax!=28h
        xor     eax, eax
        ret
     .ENDIF
     mov    eax, dword ptr [edi+12h]
     mov    ecx, dword ptr [edi+16h]
     .IF    eax!=200 || ecx!=200
        mov eax, 2
        ret
     .ENDIF

xor eax,eax
inc eax
ret
CheckIfValidBitmap endp

First, the first two bytes should be

"BM"

. This is checked in the first if/endif. Then the filesize should match the dword at offset 2 in the file. At offset 14 (edi+0Eh), the dword

00000028h

should be present. Finally, the dwords at offset 18 and 22 (12h & 16h) contain the width and height of the bitmap. Both should be 200.

If one of the checks fails, 0 is returned. If all are passed, 1 is returned. If the image size is not 200x200, 2 is returned.


11.3 - Done

You can now test your program with this bitmap:

Custom tile bitmap

(it's a GIF, so you'll have to save it as bitmap or convert it)

It should look like this (with the green color scheme on, and a little shuffled of course :)

Mosaic with custom bitmap loaded

Current project files are here: mosaic8.zip.