« Posts list

Embedded Graphics 0.4.7 and TinyBMP 0.1.0

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.

BMP format

The BMP format is pretty simple. It consists of the following sections which I'll cover separately below:

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 Header struct:

/// BMP header information
#[derive(Debug, Clone, PartialEq)]
pub struct Header {
    /// Bitmap file type
    pub file_type: FileType,
    /// Total file size in bytes
    pub file_size: u32,
    /// Reserved field 1 (unused)
    pub reserved_1: u16,
    /// Reserved field 2 (unused)
    pub reserved_2: u16,
    /// Byte offset from beginning of file at which pixel data begins
    pub image_data_start: usize,
    /// Image width in pixels
    pub image_width: u32,
    /// Image height in pixels
    pub image_height: u32,
    /// Number of bits per pixel
    pub bpp: u16,
    /// Length in bytes of the image data
    pub image_data_len: u32,
}

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:

[dependencies.nom]
version = "4.2.1"
default-features = false

The parser is pretty simple. BMP data is little-endian encoded, so we can use Nom's le_u16 and le_u32 to parse all the fields out with a function called parse_header:

named!(parse_header<&[u8], Header>,
    do_parse!(
        // "Magic bytes" marker for BMP files
        tag!("BM") >>
        file_size: le_u32 >>
        reserved_1: le_u16 >>
        reserved_2: le_u16 >>
        image_data_start: le_u32 >>
        // Skip 4 bytes: Remaining header length in bytes
        le_u32 >>
        image_width: le_u32 >>
        image_height: le_u32 >>
        // Skip 4 bytes: Number of color planes
        le_u16 >>
        bpp: le_u16 >>
        // Skip 4 bytes: Compression method used
        le_u32 >>
        image_data_len: le_u32 >>
        // Skip other fields here
        (Header{
            file_type: FileType::BM,
            file_size,
            reserved_1,
            reserved_2,
            image_data_start: image_data_start as usize,
            image_width,
            image_height,
            image_data_len,
            bpp
        })
    )
);

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.

The 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), Header only requires 28 bytes of RAM. Nice!

Bitmap information

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!

Containing everything

The other data type exported by TinyBMP is the Bmp struct:

/// A BMP-format bitmap
#[derive(Debug, Clone, PartialEq)]
pub struct Bmp<'a> {
    /// Image header
    pub header: Header,

    image_data: &'a [u8],
}

This is how you should use TinyBMP, namely with the from_slice method:

/// 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.
pub fn from_slice(bytes: &'a [u8]) -> Result<Self, ()> {
    let (_remaining, header) = parse_header(bytes).map_err(|_| ())?;

    let image_data = &bytes[header.image_data_start..];

    Ok(Bmp { header, image_data })
}

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.

Embedded Graphics

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.

The struct

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
#[derive(Debug, Clone)]
pub struct ImageBmp<'a, C: PixelColor> {
    bmp: Bmp<'a>,

    /// Top left corner offset from display origin (0,0)
    pub offset: Coord,

    pixel_type: PhantomData<C>,
}

PixelColor will be used a bit later to make ImageBmp compatible with multiple pixel types.

You can read the entire source for ImageBmp here. I'll only cover some parts of it to keep this post to a reasonable length.

Iterator setup

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 ImageBmpIterator and implementing IntoIterator for ImageBmp:

#[derive(Debug)]
pub struct ImageBmpIterator<'a, C: 'a>
where
    C: PixelColor,
{
    x: u32,
    y: u32,
    im: &'a ImageBmp<'a, C>,
    image_data: &'a [u8],
}

impl<'a, C> IntoIterator for &'a ImageBmp<'a, C>
where
    C: PixelColor + From<u8> + From<u16>,
{
    type Item = Pixel<C>;
    type IntoIter = ImageBmpIterator<'a, C>;

    fn into_iter(self) -> Self::IntoIter {
        ImageBmpIterator {
            im: self,
            image_data: self.bmp.image_data(),
            x: 0,
            y: 0,
        }
    }
}

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 next section.

The iterator

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:

impl<'a, C> Iterator for ImageBmpIterator<'a, C>
where
    C: PixelColor + From<u8> + From<u16>,
{
    type Item = Pixel<C>;

    fn next(&mut self) -> Option<Self::Item> {
        let current_pixel = loop {
            let w = self.im.bmp.width();
            let h = self.im.bmp.height();
            let x = self.x;
            let y = self.y;

            // End iterator if we've run out of stuff
            if x >= w || y >= h {
                return None;
            }

            let offset = ((h - 1 - y) * w) + x;

            let bit_value = if self.im.bmp.bpp() == 8 {
                self.image_data[offset as usize] as u16
            } else if self.im.bmp.bpp() == 16 {
                let offset = offset * 2; // * 2 as two bytes per pixel

                (self.image_data[offset as usize] as u16)
                    | ((self.image_data[(offset + 1) as usize] as u16) << 8)
            } else {
                panic!("Bit depth {} not supported", self.im.bmp.bpp());
            };

            let current_pixel = self.im.offset + Coord::new(x as i32, y as i32);

            // Increment stuff
            self.x += 1;

            // Step down a row if we've hit the end of this one
            if self.x >= w {
                self.x = 0;
                self.y += 1;
            }

            if current_pixel[0] >= 0 && current_pixel[1] >= 0 {
                break Pixel(current_pixel.to_unsigned(), bit_value.into());
            }
        };

        Some(current_pixel)
    }
}

(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 loop.

For each pixel coordinate, either 1 (for 8BPP) or two (for 16BPP) bytes are taken from the image data and cast to a u16.

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 the correct PixelColor* used in calling code. I'll explain that better in the next section.

An example

Let's walk through some of the steps to display this image on an embedded display:

Rust logo with rainbow.

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 PixelColorU16 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:

use embedded_graphics::image::ImageBmp;
use embedded_graphics::prelude::*;

let im = ImageBmp::new(include_bytes!("./awesome-image.bmp")).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 PixelColourU8 or 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.