Friday, July 3, 2015

How to read a 24bits bitmap file

Hi Guys!
Bitmap (.bmp) files are digital image files that are easy to understand in terms of its byte to byte contents. Easy as its encode only the pixel in a format widely understood by many (I believe). The pixel encoding is in RGB format which stands for RED, GREEN and BLUE. In this blog, I'll show you guys how to read a .bmp file byte and byte using Excel Visual Basic for Application (VBA) with the help of windows API.




There are three primary colors which are red, green and blue. By controlling the saturation of each of these 3 primary colors we can derive all the colors hue that can be seen by the human eye.

Take for example:

Cyan - combination of 0% red, 100% green, 100% blue (lower center color on previous)
Yellow - combination of 100% red, 100% green, 0% blue
Magenta- combination of 100% red, 0% green, 100% blue
White-- combination of 100% red, 100% green, 100% blue (all colors at 100%)

Probably, one other good example in here is the rainbow. It is a white light but thru diffraction on a prism, it's component colors will be separated. We know that the rainbow white light composes of more than one color combined:



Looking closely, the primary colors (red, green, blue) constitutes the rainbow color. The other colors such as orange, yellow, indigo and violet are just combination of saturation (hue) of these 3 primary  colors.


In digital world, the saturation is not expressed as percent (%) but rather in byte. So that, 1 byte (8 bits) red, 1 byte green and 1 byte blue. A byte has 8 bits and 11111111 or in hex 0xFF is equal to decimal 255. This means then that in digital format a 100% means 255. Rewriting then:

Cyan - 0 red, 255green, 255 blue
Yellow - 255 red, 255 green, 0 blue
Magenta- 255 red, 0 green, 255 blue
White-- 255 red, 255 green, 255 blue (all colors at 255)

This is the way it was written on .bmp file.

The format of a .bmp file has 3 parts:
1.) BMP header - from address (0x00) to 0x0D. These range of address contains the basic information regarding a bitmap file.
2.) DIB header -variable length but usually from 0x0E to 0x35. Contains the much more details regarding the image it stores.
3.) The bitmap array - this is the color data in RGB format. Since, I will discuss on 24bits. This will be in group of 3 bytes so 8bits per RGB color multiply to 3 colors (RGB) will give the 24 bits.

Anyway here's how it looks looking a bitmap file in a hex viewer:
 

The BMP Header (address 0x00 to 0x0D) is show below:


0x42 - is the ascii for letter "B"
0x4D - is the ascii for letter "M"

This means that all .bmp starts with "BM".

The next 3 bytes:

0xFA, 0xB5, 0x22 -> this is the size of the file on the disk and it is interpreted in reverse. So should be 0x22B5FA which equal to 2274810 in decimal. This is the file disk size.

The next important byte in the BMP header (to me) is the 4 bytes starting from address 0x0A to 0x0D:

0x36, 0x00,0x00,0x00 -> 0x36 is equal to 54 in decimal. This is the address of the 1st RGB bitmap data in the file.

The DIB header (address 0x0E to 0x35):



the end address is 0x35 because the BMP header says that at 0x36 address is the start of the bitmap RGB data.

Anyway, the 4 important data in here are:
1.) Image width - how many x pixel the image has. This is on address 0x12 to 0x15 (4 bytes) .

0xB9, 0x04, 0x00, 0x00 - this is stored in reverse so it is read as 0x04B9 which equal to 1209 pixels.


2.) Image Height- how many y pixel the image has. This is on address 0x16 to 0x 0x19 (4 bytes) .

0x73, 0x02, 0x00, 0x00 - this is stored in reverse so it is read as 0x0273 which equal to 627 pixels.

Note: BMP file stores file from bottom to top.

3.) The RGB encoding. This is on address 0x1C.

0x18 - this is 24 in decimal so 24 bits. This means that color is expressed as:

8 bits per color equal 24 bits total. Please take note that the bytes are stores in reverse so blue comes first then green then last will be red.

4.) Total bitmap data size. This is on address 0x22 to 0x25.

0xC4, 0xB5, 0x22, 0x00 - this is stored in reverse to it reads as 0x22B5C4 which is in decimal 2274756 bytes.


we know that we can also compute the total bitmap data size by:

Image width x image height x (bitsperpixel/8)

in which:


1209x627x(24/8)=2274129 bytes

this means that we have extra bytes of :

2274756 bytes -2274129 bytes=627 bytes

then divide by image height we have:

627/627=1 bytes

This means that each line has 1 extra bytes. This is basically called a padding -which is an extra byte added per line.


At this point we have now all the info we need:

The bitmap file has size of 1209 x 627 pixels then it has a 24bits color information of 8 bits per RGB color. The bitmap data or RGB data starts on address 0x36 and encoded with blue first, green next and red last. With total bitmap data size =2274756 and each line has extra 1 byte of padding.


In VBA, have two forms:

1.) Named "UReadBMP" with one command button named "CRead_BMP" and caption "Read".

2.) I have created another plain form named "UDisplay". My plan is that it will be the one to display the image.


Then I added below windows api on form "UReadBMP" general declarations:

'=====================================================================
Private Declare Function SetPixel Lib "gdi32" (ByVal hdc As Long, ByVal x As Long, ByVal y As Long, ByVal crColor As Long) As Long
Private Declare Function GetWindowDC Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function ReleaseDC Lib "user32" (ByVal hwnd As Long, ByVal hdc As Long) As Long
Private Declare Function GetDC Lib "user32" (ByVal hwnd As Long) As Long
Private Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, lpRect As RECT) As Long
Private Type RECT
        Left As Long
        Top As Long
        Right As Long
        Bottom As Long
End Type
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Long
Private Declare Function SetWindowPos Lib "user32" (ByVal hwnd As Long, ByVal hWndInsertAfter As Long, _
ByVal x As Long, ByVal y As Long, ByVal cx As Long, ByVal cy As Long, ByVal wFlags As Long) As Long
' SetWindowPos Flags
Const SWP_NOSIZE = &H1
Const SWP_NOMOVE = &H2
Const SWP_NOZORDER = &H4
Const SWP_NOREDRAW = &H8
Const SWP_NOACTIVATE = &H10
Const SWP_FRAMECHANGED = &H20        '  The frame changed: send WM_NCCALCSIZE
Const SWP_SHOWWINDOW = &H40
Const SWP_HIDEWINDOW = &H80
Const SWP_NOCOPYBITS = &H100
Const SWP_NOOWNERZORDER = &H200      '  Don't do owner Z ordering

'============================================================



then I created below code on form "UReadBMP" on command button "CRead_BMP" click event:


'========================================================
Private Sub CRead_BMP_Click()
Dim FileNum As Integer
Dim readbyte As String
Dim bitmapfilename As String
Dim lngFileSize As Long
Dim strBuffer As String
Dim lngCharNumber As Long
Dim strCharacter As String * 1
Dim CheckBMPString As String
Dim bmpwidth As Long
Dim bmpheight As Long
Dim bitsperpixel As Long
Dim totalbitmapsize As Long
Dim BitmapBeginningAddress As Long
Dim indexbitmap As Long
Dim padding As Integer
Dim ClientWin As Long
Dim ClientRect As RECT
Dim ClientDC As Long
Dim ClientX As Long
Dim ClientY As Long
Dim RGBResult As Long
Dim COLORREF As Long
On Error GoTo tap
bitmapfilename = Application.GetOpenFilename()
If bitmapfilename = "" Then
    Exit Sub
End If
'======OPEN THE FILE AND READ ITS CONTENT=========
FileNum = FreeFile()
Open bitmapfilename For Binary Access Read As FileNum
lngFileSize = LOF(FileNum)    'How large is the File in Bytes?
strBuffer = Space$(lngFileSize)     'Set Buffer Size to File Length

Get FileNum, , strBuffer     'Grab a Chunk of Data from the File
Close FileNum 'CLOSE THE FILE
'strBuffer as string contains the whole file data
'======Check if it's a BMP file, the 1st 2 bytes will be "BM"
 CheckBMPString = Mid(strBuffer, 1, 2)

 If UCase(CheckBMPString) <> "BM" Then
    MsgBox "Not a bitmap file"
    Exit Sub
 End If


 '=====Check if BMP is 24bits color at 1Ch=28d but 29 in here
 strCharacter = Mid(strBuffer, 29, 1)
 bitsperpixel = Asc(strCharacter)
If bitsperpixel <> 24 Then
    MsgBox "Only supports 24 bits color"
    Exit Sub
End If

 '=====Get bitmap width at 12h=18d but 18 is 19 in here bec. we start at 1 while BMP spec. address starts at 0
 For lngCharNumber = 19 To 20 'Get Bitmap size
   strCharacter = Mid(strBuffer, lngCharNumber, 1)
  
    If (lngCharNumber = 19) Then '
        'Debug.Print Hex$(Asc(strCharacter))
        bmpwidth = Asc(strCharacter)
    End If
   
    If (lngCharNumber = 20) Then
        'Debug.Print Hex$(Asc(strCharacter))
        bmpwidth = bmpwidth + (Asc(strCharacter) * 256) 'shift 8 bit left
    End If
 Next lngCharNumber

'=======Get bitmap Height starts at 16h=22d but 22 is 23 in here
For lngCharNumber = 23 To 24 'Bitmap height
  strCharacter = Mid(strBuffer, lngCharNumber, 1)
 
      
    If (lngCharNumber = 23) Then
        'Debug.Print Hex$(Asc(strCharacter))
        bmpheight = Asc(strCharacter)
    End If
   
    If (lngCharNumber = 24) Then
        'Debug.Print Hex$(Asc(strCharacter))
        bmpheight = bmpheight + (Asc(strCharacter) * 256) 'shift 8 bit left
    End If
  
    
Next
'=======Get total bitmap size starts at 22h=34d but 34 is 35 in here
For lngCharNumber = 35 To 37 'Total Bitmap size including padding
  strCharacter = Mid(strBuffer, lngCharNumber, 1)
 
      
    If (lngCharNumber = 35) Then
        'Debug.Print Hex$(Asc(strCharacter))
        totalbitmapsize = Asc(strCharacter)
    End If
   
    If (lngCharNumber = 36) Then
        'Debug.Print Hex$(Asc(strCharacter))
        totalbitmapsize = totalbitmapsize + (Asc(strCharacter) * (2 ^ 8)) 'shift 8 bit left
    End If
   
    If (lngCharNumber = 37) Then
        'Debug.Print Hex$(Asc(strCharacter))
        totalbitmapsize = totalbitmapsize + (Asc(strCharacter) * (2 ^ 16)) 'shift 16 bit left
    End If
  
    
Next
'MsgBox "width = " & bmpwidth
'MsgBox "height = " & bmpheight
'MsgBox "bitsperpixel = " & bitsperpixel
'===========Calculate the padding per line -that is extra byte per line
padding = (totalbitmapsize - ((bmpwidth * bmpheight) * (bitsperpixel / 8))) / bmpheight

'=====Get bitmap address data beginnning which is at Ah=10d which is 11d here
strCharacter = Mid(strBuffer, 11, 1)
BitmapBeginningAddress = Asc(strCharacter)

UDisplay.Show
ClientWin = FindWindow("ThunderDFrame", "Display")

If ClientWin <> 0 Then
    If (GetWindowRect(ClientWin, ClientRect)) Then
   
        If (SetWindowPos(ClientWin, 0, 0, 0, bmpwidth, bmpheight, SWP_NOZORDER + SWP_SHOWWINDOW)) Then
            ClientDC = GetDC(ClientWin)
                If ClientDC <> 0 Then
                    indexbitmap = BitmapBeginningAddress + 1 'plus 1 because we start at 1 instead of 0
                    For ClientY = bmpheight To 1 Step -1
                        For ClientX = 1 To bmpwidth
                            COLORREF = 0
                            strCharacter = Mid(strBuffer, indexbitmap, 1)
                            COLORREF = Asc(strCharacter) * 2 ^ 16 'Shift 16 bits
                            indexbitmap = indexbitmap + 1
                            strCharacter = Mid(strBuffer, indexbitmap, 1)
                            COLORREF = COLORREF + (Asc(strCharacter) * 2 ^ 8) 'Shift 8 bits
                            indexbitmap = indexbitmap + 1
                            strCharacter = Mid(strBuffer, indexbitmap, 1)
                            COLORREF = COLORREF + Asc(strCharacter)
                            'COLORREF = 65535
                            'Debug.Print "ClientX=" & ClientX & " " & "ClientY=" & ClientY & " COLORREF=" & COLORREF
'                                If COLORREF <> 65535 Then
'                                    MsgBox "here"
'                                End If
                            RGBResult = SetPixel(ClientDC, ClientX, ClientY, COLORREF)
                            indexbitmap = indexbitmap + 1 'Increment address
                        Next ClientX
                        indexbitmap = indexbitmap + padding 'For each next trace line we there's an extra byte padding
                    Next ClientY
                End If
        End If
       
    End If
End If
Call ReleaseDC(ClientWin, ClientDC)
Exit Sub
tap:
MsgBox Err.Number & " " & Err.Description
End Sub
'========================================================


You can test the code by saving a file using MSPAINT in 24 bits bitmap (.bmp) file then opening the file using the above VBA code. Here's some output I have guys:





Now, if you happen to forgot that the bitmap data is stored from bottom to top and you display the data from top to bottom then you may probably come up with:

 For ClientY = 1 To bmpheight Step 1

instead of:

For ClientY = bmpheight  To 1 Step -1


then output will be:





Anyway, the code is not yet bug free guys but I hope I was able to show you somehow how to read a .bmp file in 24bits format.

Good luck guys!!

No comments:

Post a Comment