For the past few months, I've been working on a no dependency, pure Rust library to read and write BMP files.
Why? Well, I mainly did this to learn Rust, but I've always thought file formats were pretty interesting. Video formats are pretty complicated, and more common image formats like PNG or JPEG also seemed complicated, especially because of the compression in those formats. BMP on the other hand, seemed very simple - the image was stored in a uncompressed bitmap, which was basically an array that stored the colors of the pixel. For example, if a BMP file used 4 bytes to represent the color of pixel (RGBA), and was top down (the first pixel stored was in the top left corner), then the first 4 bytes of the bitmap would be the color of the first pixel, the next 4 bytes the color of the second pixel, and so on. The simplicity makes sense, since BMP was one of the earlier digital image formats.
But the format was not as simple as I initially expected. To see why, let's go over the structure of the BMP file.
BMP formats are made of three required parts: a file header, DIB header, and array of pixels. A color table, extra bit masks, and ICC color profile are optional.
(image to illustrate structure)
I found the extra bit masks and ICC color profile to not be common or useful. The extra bit masks were only needed in a very early version of the BMP format, since later versions of the format included the masks in the DIB Header. The ICC color profile wasn't relevant to a BMP reading and writing library, since the profile was for devices to make sure the colors were displayed properly. So, those can be ignored.
We'll go over the more important ones one by one.
(image of file header format)
(link to windows docs and other docs that helped interpret DIB header)
(image of different dib header versions and what they had)
When each pixel color was represented by one byte (8 bits) or less, the pixel's bits would not directly store the RGB or RGBA table, but instead be an index that references a color stored in a color table (located after the DIB header, before the array of pixels). This would save space, but only a max of 256 colors could be used in the image, since 8 bits would mean only 256 (2^8=256) colors could be referenced. Supporting the color table increased the complexity of the code, and handling cases where the pixel color was represented by less than one byte was a pain. Come to think of it, I couldn't find any BMP images that actually had a color table. So I guess that's untested, but well, it probably works. I hope.
(image that shows color table format, along with line matching it up with pixel array data)
(image showing pixel bytes and corresponding rgba, also lines mapping to an image example)
(code, break into pieces and explain)
Despite the format being much more complex then I imagined, I don't regret writing the BMP-Rust library. Searching the documentation was frustrating, and I got stuck plenty of times. But overall, it was pretty fun, and I learned a lot.
After writing the initial code (reading DIB header, reading color of pixel at coordinate, writing color to pixel at coordinate), I was able to built upon it with fun stuff like drawing shapes (rectangles, ellipses, lines), adding filters (gaussian blur, grayscale, invert, etc), and drawing another BMP image on top. Another fun part was optimizing and speeding up the code. One huge mistake I made was using
.clone() (copying data), when I should've used references (only the pointer, which is very small, is copied, not all the data).
With BMP-Rust, I ended up going a step further and writing a web editor for BMP files, the cleverly named BMP-Editor (Github). Using Yew and WASM, I was able to write that in Rust too! But deserves it's own article.