Loading Surfaces from Custom Binary Resource Files

First off, let me give credit to Adam Hoult (wherever he may be!) for first bringing this API call to my attention. Adam, I worship you :)

Now, allow me introduce you to your new best friend, StretchDIBits:

Declare Function StretchDIBits Lib "gdi32" (ByVal hdc As Long, ByVal x As Long, ByVal y As Long, ByVal dx As Long, ByVal dy As Long, ByVal SrcX As Long, ByVal SrcY As Long, ByVal wSrcWidth As Long, ByVal wSrcHeight As Long, lpBits As Any, lpBitsInfo As BITMAPINFO, ByVal wUsage As Long, ByVal dwRop As Long) As Long
And his sidekicks..

Global Const SRCCOPY = &HCC0020
Global Const DIB_RGB_COLORS = 0
Ok, yes, I know that VB already has a vbSrcCopy constant, but I feel safer declaring my own, ok? Shutup.

As you can see by examining the StretchDIBits API call, it accepts an hDC, a bunch of X and Y values, a Long pointer to your raw bitmap data, a Long pointer to a BITMAPINFO structure, and a few flags. It will take the raw data and BITMAPINFO you provide it, stretch it according to the source and destination coordinates you pass, and blit it to the DC you give. Nifty, eh? This can be used with pictureboxes (they have DCs) as well as DirectDraw surfaces. With DDraw, you must use the DirectDrawSurface7.GetDC method to obtain the DC, and DirectDrawSurface7.ReleaseDC afterward to release it. The releasing is VERY important.. go ahead, leave it unreleased and watch your computer barf Automation Errors all over you! (Classy Lucky.. classy.)

How do you get this raw data and BITMAPINFO, you ask? Cut and paste my functions, that's how:

'Bitmap file format structures
Type BITMAPFILEHEADER
    bfType As Integer
    bfSize As Long
    bfReserved1 As Integer
    bfReserved2 As Integer
    bfOffBits As Long
End Type
Type BITMAPINFOHEADER
    biSize As Long
    biWidth As Long
    biHeight As Long
    biPlanes As Integer
    biBitCount As Integer
    biCompression As Long
    biSizeImage As Long
    biXPelsPerMeter As Long
    biYPelsPerMeter As Long
    biClrUsed As Long
    biClrImportant As Long
End Type
Type RGBQUAD
    rgbBlue As Byte
    rgbGreen As Byte
    rgbRed As Byte
    rgbReserved As Byte
End Type
Type BITMAPINFO
    bmiHeader As BITMAPINFOHEADER
    bmiColors(0 To 255) As RGBQUAD
End Type

Global gudtBMPFileHeader As BITMAPFILEHEADER   'Holds the file header
Global gudtBMPInfo As BITMAPINFO               'Holds the bitmap info
Global gudtBMPData() As Byte                   'Holds the pixel data

Sub ExtractData(strFileName As String, lngOffset As Long)

Dim intBMPFile As Integer
Dim i As Integer

    'Init variables
    Erase gudtBMPInfo.bmiColors

    'Open the bitmap
    intBMPFile = FreeFile()
    Open strFileName For Binary Access Read Lock Write As intBMPFile
        'Fill the File Header structure
        Get intBMPFile, lngOffset, gudtBMPFileHeader
        'Fill the Info structure
        Get intBMPFile, , gudtBMPInfo.bmiHeader
        If gudtBMPInfo.bmiHeader.biClrUsed <> 0 Then
            For i = 0 To gudtBMPInfo.bmiHeader.biClrUsed - 1
                Get intBMPFile, , gudtBMPInfo.bmiColors(i).rgbBlue
                Get intBMPFile, , gudtBMPInfo.bmiColors(i).rgbGreen
                Get intBMPFile, , gudtBMPInfo.bmiColors(i).rgbRed
                Get intBMPFile, , gudtBMPInfo.bmiColors(i).rgbReserved
            Next i
        ElseIf gudtBMPInfo.bmiHeader.biBitCount = 8 Then
            Get intBMPFile, , gudtBMPInfo.bmiColors
        End If
        'Size the BMPData array
        If gudtBMPInfo.bmiHeader.biBitCount = 8 Then
            ReDim gudtBMPData(FileSize(gudtBMPInfo.bmiHeader.biWidth, gudtBMPInfo.bmiHeader.biHeight))
        Else
            ReDim gudtBMPData(gudtBMPInfo.bmiHeader.biSizeImage - 1)
        End If
        'Fill the BMPData array
        Get intBMPFile, , gudtBMPData
        'Ensure info is correct
        If gudtBMPInfo.bmiHeader.biBitCount = 8 Then
            gudtBMPFileHeader.bfOffBits = 1078
            gudtBMPInfo.bmiHeader.biSizeImage = FileSize(gudtBMPInfo.bmiHeader.biWidth, gudtBMPInfo.bmiHeader.biHeight)
            gudtBMPInfo.bmiHeader.biClrUsed = 0
            gudtBMPInfo.bmiHeader.biClrImportant = 0
            gudtBMPInfo.bmiHeader.biXPelsPerMeter = 0
            gudtBMPInfo.bmiHeader.biYPelsPerMeter = 0
        End If
    Close intBMPFile
    
End Sub

Private Function FileSize(lngWidth As Long, lngHeight As Long) As Long

    'Return the size of the image portion of the bitmap
    If lngWidth Mod 4 > 0 Then
        FileSize = ((lngWidth \ 4) + 1) * 4 * lngHeight - 1
    Else
        FileSize = lngWidth * lngHeight - 1
    End If

End Function

Hm.. this'll take some explaining :) Lets start at the top. The structures you see declared are the standard components of any bitmap file. They are used in the ExtractData subroutine to piece together the bitmap from within the binary file.

ExtractData accepts two arguments, the first (strFileName) is the name of the file we'll be extracting from, the second (lngOffset) is the offset within the file where our desired bitmap's data begins. This function can be used on custom binary resources AND standard bitmap files, it can't really differentiate between the two so long as you pass the correct offset (lngOffset = 1 for standard bitmaps).

Once we have these two pieces of information, we can go ahead and open up the file and extract the gudtBMPFileHeader information from the lngOffset location given. Next, gudtBMPInfo.bmiHeader is extracted in a similar fashion, but after that things get dicey. We'd like to obtain the colour table data (if this is an 8bit bitmap), but not all programs format this data in the same fashion. Some will store only a set number of entries, as indicated by the gudtBMPInfo.bmiHeader.biClrUsed variable. If this value is zero however, we can assume a full complement (256, if 8bit) of colours and can extract merrily. Otherwise we have to loop through each value and extract it manually :(

Next on the agenda is the raw bitmap data. We must size our gudtBMPData byte array appropriately, and this can be tricky with 8 bit bitmaps. You see, ALL bitmap scan lines (horizontal lines of pixels) must end on a 32bit boundary, so they are sometimes padded with blank bits. To account for this quirk, I created the FileSize function that'll calculate a bitmap's true size based on its percieved width and height. Use it well :) 24bit bitmaps don't give us this trouble, and for them we can assume that the gudtBMPInfo.bmiHeader.biSizeImage value is accurate.

All that remains is to Get our gbytBMPData array, and voila! We have a bitmap in memory! You may notice I perform a few other functions on 8bit bitmaps after the final Get. This is simply to correct for those silly programs that create 8bit bitmaps with fewer than 256 colours. Such bitmaps are slightly slower to load, so my function modifies them so they are of the faster format.

The hard work is done! We can reap the rewards:

StretchDIBits lngDC, 0, 0, gudtBMPInfo.bmiHeader.biWidth, gudtBMPInfo.bmiHeader.biHeight, 0, 0, gudtBMPInfo.bmiHeader.biWidth, gudtBMPInfo.bmiHeader.biHeight, gudtBMPData(0), gudtBMPInfo, DIB_RGB_COLORS, SRCCOPY

Get your DC (from your DDraw surface or picturebox), place it in lngDC and the above line of code will handle the rest. It blits (with no stretching) the bitmap you've loaded into memory onto the DC you've passed it! No more DirectDraw7.CreateSurfaceFromFile for you!

If you are thoroughly confused, check out my sample source.

And for my next trick...