Let’s learn Rust, shall we?

Having worked in one Swedish company for almost 3 years I wrote a lot of C code. I somewhat enjoyed it because the project codebase contained several amenities. It contained functions for 'strings' manipulation, functions for database manipulations, and some memory checks. I rarely needed to use malloc, and I’ve only seen a segmentations fault maybe a couple of times.

I knew I could have it worst, but I still searched for some alternatives. Along GO [1], I also found Rust.

Rust is a multi-paradigm programming language designed for performance and safety, especially safe concurrency. Rust is syntactically similar to C++, but can guarantee memory safety by using a borrow checker to validate references.

— Description from Wikipedia

Sounds good to me. Mozilla Research developed it about 10 years ago, and many big products like Firefox, Dropbox, and Cloudflare use it today.

I feel convinced, so let’s learn it and by learning it I don’t mean starting with a tutorial. By learning I mean creating a small project. I enjoy playing with file formats so why not write a BMP decoder in Rust?

First steps

I installed Rust compiler on my Ubuntu with sudo apt install rustc.

g70KZN6

To create a new project I used cargo command cargo new bmp-rust.

This command created a new source file src/main.rs with 'Hello World' in it:
fn main() {
    println!("Hello, world!");
}

Showing an image using a standard library

Before I jump into the decoding part I want to learn how to display an image without my decoder.

I googled rust show image to get some example code. In Rust documentation [2] I found this example:

use show_image::{ImageView, ImageInfo, create_window};

#[show_image::main]
fn main() -> Result<(), Box<dyn std::error::Error>> {

  let image = ImageView::new(ImageInfo::rgb8(1920, 1080), pixel_data);

  // Create a window with default options and display the image.
  let window = create_window("image", Default::default())?;
  window.set_image("image-001", image)?;

  Ok(())
}

I ran it and it did not work. I got the following error.

APYZqVf

I’ve tried using cargo to search for this dependency and then install it.

3XMXq83

I cannot install it that way, but I must add it to project dependencies, good hint!

I edited my Cargo.toml file and I’ve tried again.

zLC1AQi

This time I got an error about pixel_data variable (getting a different error always feels heartwarming).

Wwz8EPy

I guess the example from the documentation missed this variable, bummer.

After more googling how to get the pixel data from a bmp file I found a solution.

let path = "/home/matishadow/.config/JetBrains/Rider2020.3/scratches/bmp-rust/imgs/test-image.bmp";

let data = fs::read(path).unwrap();
let dynamic_image = ImageReader::open(path)?.decode()?;

let rgb_image = dynamic_image.to_rgb8();

As a test image I’ve used a standard portrait of Lena. 4z1ylwJ

I run the solution and a window with black screen greeted me. Console reported a strange error INTEL-MESA: warning: Haswell Vulkan support is incomplete. I dug deep into Google and I couldn’t find a right solution for it for a long time. Finally after an hour of searching I found this comment. Af4quym For me vkcube ran perfectly but still the image did not show itself. Nevertheless I tried to removed this intel_icd.x86_64.json file and after that it magically worked.

Great success, now I can see Lena instead of the black screen, but will I be able to duplicate this result using my decoder?

Decoding part

Let’s google how the BMP format looks like. I found this page, which explains the format simply.

This file format looks something like this:

xZiwhYs

I used information on the page to create structs describing the format.

struct BmpHeader {
    header_type: [char; 2],
    size: u32,
    reserved1: u16,
    reserved2: u16,
    offset: u32,
}

struct InfoHeader {
    size: u32,
    width: i32,
    height: i32,
    planes: u16,
    bits: u16,
    compression: u32,
    image_size: u32,
    x_resolution: i32,
    y_resolution: i32,
    n_colors: u32,
    important_colors: u32,
}

struct BmpFile {
    header: BmpHeader,
    info_header: InfoHeader,
    data: Vec<u8>,
}

I skipped the optional palette part since well it’s optional.

Filling in the structs

To fill in these structs I used bit manipulation operations. For example to fill in the size field I took 4 bytes of file data and put them in little-endian order. [3]

impl BmpHeader {
    pub fn new(bytes: &Vec<u8>) -> Self {
        Self {
            header_type: [bytes[0] as char, bytes[1] as char],
            size: ((bytes[5] as u32) << 24 | (bytes[4] as u32) << 16 | (bytes[3] as u32) << 8 | bytes[2] as u32) as u32,
            reserved1: 0,
            reserved2: 0,
            offset: ((bytes[13] as u32) << 24 | (bytes[12] as u32) << 16 | (bytes[11] as u32) << 8 | bytes[10] as u32) as u32,
        }
    }
}

impl InfoHeader {
    pub fn new(bytes: &Vec<u8>) -> Self {
        Self {
            size: ((bytes[17] as u32) << 24 | (bytes[16] as u32) << 16 | (bytes[15] as u32) << 8 | bytes[14] as u32) as u32,
            width: ((bytes[21] as i32) << 24 | (bytes[20] as i32) << 16 | (bytes[19] as i32) << 8 | bytes[18] as i32) as i32,
            height: ((bytes[25] as i32) << 24 | (bytes[24] as i32) << 16 | (bytes[23] as i32) << 8 | bytes[22] as i32) as i32,
            planes: ((bytes[27] as u16) << 8 | bytes[26] as u16) as u16,
            bits: ((bytes[29] as u16) << 8 | bytes[28] as u16) as u16,
            compression: ((bytes[33] as u32) << 24 | (bytes[32] as u32) << 16 | (bytes[32] as u32) << 8 | bytes[30] as u32) as u32,
            image_size: ((bytes[37] as u32) << 24 | (bytes[36] as u32) << 16 | (bytes[35] as u32) << 8 | bytes[34] as u32) as u32,
            x_resolution: ((bytes[41] as i32) << 24 | (bytes[40] as i32) << 16 | (bytes[39] as i32) << 8 | bytes[38] as i32) as i32,
            y_resolution: ((bytes[45] as i32) << 24 | (bytes[44] as i32) << 16 | (bytes[43] as i32) << 8 | bytes[42] as i32) as i32,
            n_colors: ((bytes[49] as u32) << 24 | (bytes[48] as u32) << 16 | (bytes[47] as u32) << 8 | bytes[46] as u32) as u32,
            important_colors: ((bytes[53] as u32) << 24 | (bytes[52] as u32) << 16 | (bytes[51] as u32) << 8 | bytes[50] as u32) as u32,
        }
    }
}

I know this code looks ugly, and I should have written some bit converter, but I kept it like this for simplicity.

To decode the struct describing the file I used the two methods from above.

impl BmpFile {
    pub fn new(bytes: &Vec<u8>) -> BmpFile {
        let header = BmpHeader::new(&bytes);
        let info_header = InfoHeader::new(&bytes);
        let data = &bytes[header.offset as usize..bytes.len() as usize];

        return Self {
            header,
            info_header,
            data: data.iter().cloned().collect(),
        };
    }
}

I used offset field from the header part to know the offset of image data. To extract this data from the file I used this expression &bytes[header.offset as usize..bytes.len() as usize];.

Show time

I changed the code so instead of standard ImageReader it makes use of my new decoder. I created the image_view in the following way.

let image_view = ImageView::new(
    ImageInfo::rgb8(bmp_file.info_header.height as u32, bmp_file.info_header.height as u32),
    &bmp_file.data);

Everything seems fine so let’s run it.

8IU2Jdv

I mean it looks similar but yet totally off. First the colors seem incorrect. Not only this but also my decoder inverted the image in a strange way.

Let’s start with fixing the colors. When creating the image_view I used ImageInfo::rgb8 but the order of pixel data is also little-endian.

After changing it to ImageInfo::bgr8 I reran the program.

iT5Pcjf

Progress! The colors look good now but it’s still inverted, hmm.

Have I done anything wrong? I just read the image data as it is. Let’s google it.

In this stackoverflow post I found a little of history about the bitmap format.

So, in DIBs, the bottom row of the image is the first row of the file, and the top row of the image is the last row in the file. This is called a bottom-up organization. Because this organization is counterintuitive, you may ask why it’s done this way.

Well, it all goes back to the OS/2 Presentation Manager. Someone at IBM decided that all coordinate systems in PM—including those for windows, graphics, and bitmaps—should be consistent. This provoked a debate: Most people, including programmers who have worked with full-screen text programming or windowing environments, think in terms of vertical coordinates that increase going down the screen. However, hardcore computer graphics programmers approach the video display from a perspective that originates in the mathematics of analytic geometry. This involves a rectangular (or Cartesian) coordinate system where increasing vertical coordinates go up in space.

In short, the mathematicians won. Everything in PM was saddled with a bottom-left origin, including window coordinates. And that’s how DIBs came to be this way.

I guess I need to write some more code to invert the image data row by row.

let mut inverted: Vec<u8> = Vec::new();
for i in 0..height {
    for j in 0..width {
        inverted.push(bmp_file.data[(((height - i - 1) * width + j) * 3) as usize]);
        inverted.push(bmp_file.data[(((height - i - 1) * width + j) * 3 + 1) as usize]);
        inverted.push(bmp_file.data[(((height - i - 1) * width + j) * 3 + 2) as usize]);
    }
}

Running it and finally a success! I’ve decoded the BMP image, and I’ve displayed it correctly!

xxVgNtz

1. Go is a statically typed, compiled programming language designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. Go is syntactically similar to C, but with memory safety, garbage collection, structural typing, and CSP-style concurrency.
3. A little-endian system, in contrast, stores the least-significant byte at the smallest address.