BMP: the simple, underappreciated image file format

3 December 2020
6 mins

In the early days of Ink, the most interesting thing Ink programs could do was take some textual input, and output some text back to the terminal. While that was useful for testing the language, it was far from interesting. So once the basics of the language were up and running, I wanted a way to render images from Ink programs. After some research, I settled on BMP as my file format of choice, and wrote bmp.ink, a tiny BMP image encoder in about ~100 lines of Ink code.

A render generated by bmp.ink of the Mandelbrot fractal set

Armed with this new library, Ink could do so many more cool, creatively interesting things, like generate graphs, render charts, and compute a Mandelbrot set into a beautiful graphic (like the one above), all without depending on other external tools.

See bmp.ink on GitHub →

This is the story of why I chose BMP as my file format, how bmp.ink came to be, and why this vintage file format is a diamond in the rough for small toy programming projects.

Image file formats: a subjective taxonomy

Like any topic in computing, designing an image file format is a game of tradeoffs. The most popular file formats, like JPG and PNG, optimize for image fidelity, speed, and file size. Other formats, like SVG, specialize for certain kinds of images like vector graphics. Formats for professional graphics workflows sometimes sacrifice everything else at the cost of image quality and cross-compatibility with other software.

When I set out to write an image encoder in Ink, I knew from the start that the most common formats like JPG and PNG wouldn’t be ideal. Both are excellent file formats with decades of research behind them, but encoding JPG and PNG images aren’t trivial – they depend on some clever math like discrete cosine transforms and Huffman coding to trade off file format complexity for file size. But for me, the #1 priority was implementation simplicity. I wanted to build an encoder quickly, so I could get on with building things that used the library to generate interesting images. This meant I needed a format that did as little as possible to compress or transform the original image data, given as a grid of RGB pixel values.

On the other end of the convenience-practicality spectrum are image formats based on text files, like the PPM image formats. PPM images were designed so they could be shared as plain text files – PPM images store color values in the file for each pixel as strings of numbers. This makes PPM files easy to work with in any language that supports robust string manipulation, but because PPM is a more obscure format that never saw widespread general use, not all operating systems and image viewer software supports it. For example, on the Macbook I was working with, the native Preview app couldn’t open PPM files. I could have used another library or piece of software to translate PPM files to a more popular format like PNG, but that felt unsatisfying, like I was only solving a part of the problem at hand.

Searching for a format that fit the balance I needed between simplicity and compatibility, I found the BMP file format. BMP is a raster image file format, which means it stores color data for individual pixels. What sets BMP apart from other more common formats is that BMP is not a compressed image format – each RGB pixel is stored exactly as a 3-byte chunk of data in the file, and all the pixels of an image are stored sequentially in the file, usually in rows starting from the bottom left of the image. An entire, real-world BMP file is just a big array of pixel data stored this way, prefixed with a small header with some metadata about the image like dimensions and file type.

This format is much simpler than JPG or PNG! It’s quite possible for any programmer to sit down and write an encoder that translates a list of RGB values into a BMP file format, because the format is such a straightforward transformation on the raw bitmap data of the image. As a bonus, because BMP images were quite common once, most operating systems and image viewers natively display BMP files (the last image on this post is a BMP file, displayed by your browser). So it was decided – the first image encoder in Ink would support BMP output.

Writing a bitmap image encoder in Ink

BMP, like many image formats, comes in a few different variations. A part of the header portion of a BMP file is to disambiguate between these different types of BMP files. For implementation simplicity, because I only needed an encoder and not a decoder, I chose early on to support only one particular variation of BMP. This meant that I could hard-code many of the header values, like color formats and compression information, which further simplified my prototype.

Once my library supported the basic BMP header, which contains information about the image dimensions and color formats, I started testing it, generating some (very) small image files, like this 4-pixel square in 70 bytes.

Testing BMP in the shell Four colored pixels in a BMP image

Once I had an image rendering on my screen (which took some debugging!), I could experiment with different image sizes and pixel values to generate more interesting pictures. Here’s one with a 4x4 image, filled with a color spectrum.

Testing BMP in the shell 16 colored pixels in a BMP image

Once the basic image encoder was working, I could start experimenting with what I was putting on the screen. This is the fun part – creating algorithms and patterns with basic colors and shapes that made interesting designs! Here’s one of the higher-resolution images I had saved, with overlapping rainbows masked over with a circle.

A test BMP image with some rainbows and shapes

All this – going from absolute zero to a working prototype image encoder – took me one evening of work. I think this speaks to the importance of embracing rough, minimal MVPs, and choosing smart constraints for your prototype. Had I chosen a different image format, or had I not chosen to hard-code initial header parameters, the project would have taken much longer. But because I raced to a simple, functional MVP, I could start actually rendering images and trying interesting experiments, which was my goal in the first place.

Mandelbrot set rendered with bmp.ink

Since I initially wrote the BMP library, I’ve worked on several Ink projects building on it, from graphing calculators to Mandelbrot set renderers to other interesting artistic experiments. Most notably, Traceur, my path tracing program written in Ink, generates BMP images using this BMP library I wrote late one night.

A path-traced 3D scene generated with bmp.ink

BMP used to be quite ubiquitous in the early 2000s, and that ubiquity means that almost all operating systems and browsers still open and display BMP images natively, even though the file format has been superseded in common use by more efficient formats like JPG and PNG. Despite its aging fate, though, I think the BMP file format is quite a diamond in the rough. A simple, easy-to-implement image file format ideal for small projects and experiments.


Macro elegance: the magical simplicity of Lisp macros

Implementing the lambda calculus in Ink