Tuesday, September 29, 2026

Bitmap (BMP) Files


Hi there. I initially had started this article as the second part in the VGA Palette series, but in this article, I've decided to focus on the bitmap (.bmp) file viewer, which I originally developed for the second part and consequently on bitmap files. That way, I can get back to VGA palette operations in mode 13h in the next article, without losing focus with file format specifications.


BMP File Format

In my previous post, I mentioned that I was developing a tiny bmp file viewer in C for mode 13h. Bitmap (bmp) files have a quite simple structure, so that I can cover the necessary aspects of this topic in this article.

BMP files consist of a simple two-part file header and pixel data. The first header contains the file size and a pointer to the image data. The second header contains information such as width, height, color depth and compression information. On the right figure, the first header is the area marked in red, while the second header is the area marked in yellow. The gray area shows the palette, which I will discuss later in this post. This figure shows a hex dump of an image with 320x200 pixels. I've marked fields such as width and height with boxes.

Technically speaking, BMP files can be compressed with RLE (run length encoding), but it's very unusual to find a compressed one. Even though the format was developed by Microsoft and IBM, not even MS Paint(brush) was able to save compressed BMP files, if I recall correctly. For this reason, I won't discuss RLE at all.

Although I don't want to go into the details of the code in this chapter, it will be still easier for me to explain the file format structures using the data structures in the code's header file ( VGA13.H ). At the very beginning of a BMP file is 'BM' file signature. The only important field in the bitmapfileheader structure is the OffsPixelArray pointer, which points to the pixel array, the area shown in the figure above with the values 36h, 04h. This is followed by bitmapimageheader, which holds the image-related info. I've also specified the file offsets of these fields in the VGA13.H file.

According to the BMP file format article, there are seven different versions of bitmapimageheader structure. The 40-byte version called BITMAPINFOHEADER, which I also used as a basis of my code, is the most common version. I tested my code with 23 files, some saved with various image processing software including GIMP, and some downloaded from internet. I cannot include one of these files, but I uploaded my test set of 22 files. Btw, even though GIMP has developed the latest version of bitmap header, BITMAPV5HEADER, it does not use it by default when saving bmp files.

Bitmap files support four different color depths. These are stored in BitsPerPixel field. If BitsPerPixel is;

  • 1, then the image has two colors, black and white and each byte contains 8 pixels.
  • 4, then the image is 16-color, and it contains a palette for these colors. Each byte contains two pixels.
  • 8, then the image has 256 colors and a palette for these colors. Each byte represents a single pixel.
  • 24, then each pixel of the image consists of R, G, B triplets each with 8 bits. And the image doesn't contain any palette.

I focused only on 8 bit per pixel images. Because, first, I can't display more than that with mode 13h, and second, the palette is only available for 4 and 8-bit color depths. I created some of the images in my test set for this article, by resizing the image of three parrots siting on a wire mesh fence from VGA article in Wikipedia to 320x200.

Direct Link

I need to make a note about 8-bit images here. Logically, grayscale palette consists of (i, i, i) RGB triplets, where i is their index. Therefore, there is no need to save the palette along with the file. I knew, that if a bitmap file has 8-bit color depth but no palette, it's treated as grayscale image, and some programs still used to save the palette in the file for compatibility anyway. A grayscale image, I saved with GIMP has been saved with a palette, and even after manually removing the palette from the file, and trying to open it with four different programs, it did not succeed. Moreover, Wikipedia mentions that a palette is mandatory for files with a color depth of 8 or lower. On the other hand, the bmp_08.bmp file in the test set is an example for such an 8-bit file without palette. In short, I think, this is a matter on which there is still no consensus. From coding perspective, this worked out quite well for me, as I did not have to do anything extra for this case. My code opens this file, but since it doesn't know that it needs to load a grayscale palette, it opens it with the default mode 13h palette.

Palette follows the bitmapimageheader structure. The size of bitmapfileheader is fixed 14 bytes. The size of bitmapimageheader is given in HeaderSize field. Therefore, the offset of the palette can be found by adding 14 to this value. If this sum equals to bitmapfileheader.OffsPixelArray value, it indicates that there is no palette (e.g. for 24-byte color depth). Each palette entry consists of 4 bytes representing R,G,B and A respectively. However the alpha channel and transparency implementation are not standard in bmp files. Therefore A is nearly always zero. And in total, the number of palette entries is 2BitsPerPixel.

Palette must end at bitmapfileheader.OffsPixelArray offset, and is followed by pixel array. Here, the pixels are stored from left to right and from bottom to top. The first data corresponds to the bottom-left pixel. If the image's width is not a multiple of four, the rows are padded with zeros to make them a multiple of four. For example, if an image is 133 pixels wide, three zeros are added at the end of each line while saving, to pad the data to 136.

Screenshot from DOSBox

BMP Viewer

Above is an example, demonstrating the programs output.

I will review my code starting with the main(...) function on line 174. Here, the memory is allocated statically for both file headers first (lines 177-178). If a BMP file has not been passed to the program as a command line parameter, the user is asked for the file name (line 188), and the file is then opened for reading (line 193).

The readFirstHeader(...) function reads from the file directly into the data structure, and if the file signature does not equal to 'BM' (line 43), it exists with a non-zero error and this also terminates the program (line 199).

The readSecondHeader(...) function on line 199, similarly reads the second header from the file into the data structure. To determine file properties easily, I added a small showBMPinfo function (line 22) to print them. This function call can be commented out or combined with a debugging parameter. If the file does not meet the specifications we can display, i.e. if the color depth is different than eight or if the file is compressed, the function exits with an error value and it terminates the program. On line 69, a warning is printed to the screen if the size of the image is bigger than 320x200, but the file is opened anyway and viewed partially.

I simply implemented mode13() and textmode() functions with assembly (line 78 and 86).

As I mentioned above, there are multiple versions of bitmapimageheader. Common fields of these header versions are width, height and color depth. If the header of the input file happens to be something different than BITMAPINFOHEADER (maybe a header of a different length), the readSecondHeader(...) function will not read the header completely. For this reason, on line 207, any unread bytes are skipped. The file pointer is moved to the start offset of the palette, and processing of the palette begins with setPalette(...). In this function, the palette is read ColorCount times 4-byte chunks. On line 105, the color index is written to port 0x3C8, followed by R, G, B values written to port 0x3C9, as I explained in the previous post. Since VGA colors are 18-bit, the 8-bit values are shifted right two bits to convert them to 6-bit.

The fseek(...) on line 210 is there to read the correct offset, if the file's header version is not BITMAPINFOHEADER. Normally, the file pointer should already point to the firstHeader.OffsPixelArray value at that moment. setImage(...) reads the pixel array and renders the image to the screen. I wrote this function twice. The first version on line 134 is inefficient, because it reads the byte by byte. With the second version, images are read line by line. For this, I round the width up to the nearest multiple of 4 using ceilTo4 and allocate a memory line of that size (line 160). Finally, I read the bytes from the file with image width sized blocks (line 167) and write them to the screen with a for loop. Instead of PutPixel function, something like this could have been done here:

lds si, linebuffer
push A000
pop es
mov ax, i
mov di,ax
shr ax,8
shr di,6
add di,ax
mov cx, c4
rep movsb

This snippet is of course just a sketch. The linebuffer pointer is loaded into DS:SI. i is multiplied by 320 using shr and loaded to DI. Image width is loaded to CX, and the linebuffer could be copied block by block to VGA screen memory using rep movsb. In this case, the MIN macro should also need to be translated to assembly. In this way, it should run even faster than my recent approach, yet I did not implement it, because I did not want to deal with assembly and far pointers in C, which could have increased debugging time significantly.

No comments:

Post a Comment