Embedded graphics 0.4.7 has been released, along with a new sister crate, tinybmp! TinyBMP aims to parse BMP-format image data using no dynamic allocations. It targets embedded environments but can be used in any place a small BMP parser is required. Thanks to TinyBMP, Embedded Graphics now supports loading this simple image format. The header photo was made using Embedded Graphics and the SSD1331 driver in pure Rust. In this post, I'll talk through how the BMP file is parsed in no_std environments with nom and how to get BMP images working with embedded_graphics.
The BMP format is pretty simple. It consists of the following sections which I'll cover separately below:
- File header (metadata)
- DIB header (more metadata)
- Colour pallette table (for colour-indexed images, ignored by TinyBMP)
- Bitmap information
🔗File header and DIB header
I'm not sure why the header is split into two parts, but TinyBMP treats them as a single section as
they form a contiguous block of bytes in the file. The parser ignores some extraneous fields in the
header. The ones we're interested in are described in the
/// BMP header information
There are some other fields present in the header segments, but they're not important for bitmap parsing.
To parse this header, I'm using nom (a parser-combinator library) in
no_std mode. It's defined in TinyBMP's
Cargo.toml like this:
The parser is pretty simple. BMP data is little-endian encoded, so we can use Nom's
le_u32 to parse all the fields out with a function called
This takes a slice (
&[u8]) and produces a
Header struct from the first few bytes of it. It
should be pretty self explanatory, but I'll address some odd parts of the code above.
tag!("BM")This is the "magic bytes"
BMmarker for BMP files, and must be present for the BMP to be valid.
- Lines with a lonesome
le_u32simply skip ahead 2 or 4 bytes respectively. This will be a field in the header that we want to ignore.
Header struct is all we need to parse from the BMP file to be able to use it. On 32 bit
systems (i.e. ARM MCUs, other embedded devices),
requires 28 bytes of RAM.
BMP pixel data is pretty unremarkable, the only caveat being that the Y values are inverted, with the bottom row at the start of the data. Pixel values are stored in little-endian order in whatever bit depth is specified in the header. This will commonly be 32 or 24 bits, but 16, 8 or even 1BPP is supported by the BMP format. Currently, only 8 and 16 BPP images are supported by Embedded Graphics. If you'd like to see other bit-depths supported, please open an issue!
The other data type exported by TinyBMP is the
/// A BMP-format bitmap
This is how you should use TinyBMP, namely with the
/// Create a bitmap object from a byte array
/// This method keeps a slice of the original input and does not dynamically allocate memory.
/// The input data must live for as long as this BMP instance does.
This method takes a slice representing a complete BMP file and creates a
Bmp from it. The header
is parsed and a sub-slice of the input data is kept in the
image_data field, based on the
image_data_start field from the header.
Keeping with the no_std, low-memory-usage theme,
Bmp only consumes 48 bytes of memory!
Bear in mind the playground uses 64 bit Rust (I think), so memory usage on a 32 bit ARM
microcontroller might even be a bit less. The original bitmap data must be kept somewhere of course,
but this works well with
include_bytes!() in and embedded context;
include_bytes!() data is kept
in flash memory, leaving precious RAM available for your application. Microcontrollers generally
have a lot more flash than RAM, so it's sensible to store large byte arrays in flash.
TinyBMP exposes BMP image data through the
image_data() method on
Bmp. We can use this image data to form an
Image iterator for Embedded Graphics to use.
First, let's define
ImageBmp that wraps a
Bmp file with some extra data to allow for cool
Embedded Graphics things like transforms:
/// BMP format image
PixelColor will be used a bit later to make
ImageBmp compatible with multiple pixel types.
You can read the entire source for
I'll only cover some parts of it to keep this post to a reasonable length.
Now we have an
ImageBmp, we need to get an iterator for it so the pixel data can be used by
Embedded Graphics-compatible libraries. This can be done by creating
IntoIterator for ImageBmp:
image_data uses the
Bmp::image_data() method mentioned above. Note that everything uses
references to the original data, meaning no new allocations of huge amounts of pixel data.
I'm also adding
From<u8> + From<u16> as trait bounds for reasons that will become clear in the
This is the most important part of the compatibility between Embedded Graphics and TinyBMP - the
iterator implementation. It's responsible for iterating over the BMP pixel data correctly (remember
that it starts from the bottom up) as well as converting 8 or 16 bit words into the correct
PixelColor*. This is what it looks like:
(it could probably stand to be optimised a bit. PRs welcome!)
This function is responsible for stepping through on screen pixels; a translation may put some of
the image off the top left corner of the screen, so those pixels must be skipped. This is why most
of the function body is wrapped in a
For each pixel coordinate, either 1 (for 8BPP) or two (for 16BPP) bytes are taken from the image
data and cast to a
If the pixel has positive coordinates, it's position and colour are returned. This is where the
From<u8> + From<u16> bounds come in - they allow
bit_value.into() to cast the pixel value into
PixelColor* used in calling code. I'll explain that better in the next section.
Let's walk through some of the steps to display this image on an embedded display:
The SSD1331 crate uses the new TinyBMP support in Embedded
Graphics to draw colour images like the one in the header for this post. The SSD1331 is a 16 bit
colour display. It requires pixel data to be sent to it as 16 bit words split into 5 red, 6 green
and 5 blue colour bits. This works nicely as we can use
to iterate over an image correctly.
First, we need an RGB565 image. The GIMP can export these quite easily - go to File -> Export As or
press Shift + Ctrl + E and save as a file ending in
.bmp. The BMP
export options dialog will now show up. Make sure you choose
Advanced Options ->
16 bits ->
R5 G6 B5 option when exporting:
Now we have a 16 bit BMP, it can be loaded into a Rust program using
include_bytes!(), like this:
let im = new.unwrap;
im can now be used to draw to a display compatible with Embedded Graphics with a simple
disp.draw(im.into_iter()) command. There's a complete example
here in the SSD1331 examples folder
if you want to see a complete program.
Because Rust is awesome, it should be able to figure out whether you intended to use
PixelColorU16 based on the type of pixel the display driver uses. 8BPP BMP images will be
converted to either type, and 16BPP BMP images will automatically convert two-byte words into a
PixelColorU16 which should make more images compatible with more displays.
And that's it! BMP support is great for Embedded Graphics, as it's now far simpler to work with images that are used in embedded projects. Hell, it's even just nice to get an image preview in the file browser! More image formats will be added in the future, namely ones that support simple compression like TGA et al, so stay tuned.