BMP Image Rendering, in Rust
It has been awhile since I've posted a tutorial. Don't worry! I am not ditching the series I have started.
I've just been busy doing a bit more...advanced concepts(coughosdevcough).
But, today I am going to bring to you BMP Image Rendering, in Rust!
It was allot of fun to go about creating this application in Rust when I had free time apart from working on my OS.
But, I decided I'd take a mental break, and get back to uploading tutorials.
So, without further ado, lets get into this!!
Lets look back over the BMP file format.
The BMP file format can differ from image to image. Some images may have compression(which causes the headers to contain more information), others are simple.
In our case, we will be keeping it simple.
The very first 14 bytes you will see, also known as the BMP file header, contains the following information:
- The first 2 bytes are assigned to 'B' and 'M'
- The next 4 bytes are the file size, in bytes
- There are 4 bytes of padding
- The next 4 bytes is the pixel array starting address
Then, there is the DIB header, which contains more vivid information, like the color palette, the compression, resolution etc.
The DIB header has the following syntax:
- The first 4 bytes are the header size(normally 40)
- The next 4 bytes is the width of the image
- The next 4 bytes is the height of the image
- The next 2 bytes is the number of color planes
- The next 2 bytes are the bits per pixel
- The next 4 bytes is the compression method(we'll ignore this)
- The next 4 bytes is the image size
- The next 4 bytes is the horizontal resolution
- The next 4 bytes is the vertical resolution
- The next 4 bytes is the color palette
- The next 4 bytes represent the number of important colors
Keep note, the 2 bytes of color planes and the 2 bytes for the bits per pixel will be combined into one number, to keep it at a constant of 4 bytes.
I might upload a tutorial going over the more advanced concepts of BMP image rendering
I personally use Linux as my virtual machine. But I do not believe that the setup of a Rust project is any different.
We are going to be using cargo throughout this tutorial. So, go ahead and make a new directory of whatever name you want, and simply type
cargo init at your command line.
This will provide you with a few things:
- A /src folder
- A cargo.lock file
- A cargo.toml file
And, of course, the /target folder with is just information about the executable.
Lets start the code!
I like formatting my Rust code that of how you would organize C code. Normally, when you write C, you will have a header file that pre-defines all the code you will use within the C file.
This is how I organize my code. Feel free to do it as you please.
So, following along in my footsteps, lets create a file called...render. Why not? :)
render.rs, we will need a few things. To keep the code organized instead of just working with an array throughout the whole program, we are going to implement structs for each bit of the program. These structs will help us easily create that array we will need to ultimately create the BMP image.
We aren't going to be adding anything too fancy in this tutorial, like configuring where a specific pixel goes. Instead, we are simply going to take in an array of pixels and just throw them into the BMP image. For simplicity, of course!
The "header" file
There are a few things we will need to "import". Firstly, we need a way to check if a file exists or not. In Rust, we can use
PathBuf to do so. Next, we need a way to throw an error if something fails while working with the file:
use std::path::PathBuf is simply telling Rust to use
PathBuf instead of us having to consistently write
std::path::PathBuf everytime we want to use
PathBuf. Same with
PathBuf has a many functions, but we will only be using a few for our case:
We will be using the function
from_path. This function simply just takes in a string, and converts it into a
PathBuf type. This will enable us to use the next function
exists. This function returns a boolean. I think this function is quite self explanatory.
Now, the first thing in every BMP image is the BMP file header. This is a simple struct. Nothing too advanced. This struct will store information about the header field('B', 'M'), the file size, and the starting address.
#[derive(Debug, Clone)] is a common thing you will see throughout this tutorial. Rust is a bit iffy with how they allow variables to be used throughout the program. I believe this has something to do with static memory. Correct me if I am wrong.
Simple what that line is saying is, allow us to print this struct using the print macro, and allow us to be capable of crating a "copy" of this struct, just in case we want to assign it to another variable.
Next up, the DIB header. This is a bit more lengthy than the BMP file header. This struct will contain information about the header size, with width & height, color planes, bits per pixel, compression, image size, horiz res, vert res, color palette and the number of important colors.
Now, these two structs on there own won't help us any. So lets compromise this and allow ourselves to create another struct that will contain all this information, in one place.
Why do this? We want to keep our code organized. We wouldn't want a long struct with all the information. Not only would it look sloppy, but it would be hard to keep track of. Using different structs and allowing ourselves to create a single struct that can then hold the values of these other structs will be easier on us. In a sense that, we can do
This struct will hold 3 arrays. The BMP file header array, the BMP DIB header array, and the pixel array.
Now, what if we run into an error within the application. How do we successfully store information about the error?
What I have done is I have implemented a struct to store information about the error we have. This is not needed, however it was a good idea.
This struct will enable us to see what file the error occurred on, the information about the error, and also enable us to overlook the data.
Now, what use is there of a struct meant to store information about errors...if we don't have any errors defined?
An enum in Rust is largely used for self-defined errors.
In our case, we want a few self-defined errors:
FileError will be thrown whenever there is an error with working with a file.
ErrCreating will be thrown when we simply have an issue with creating the BMP image.
InvalidImageSize is used if the pixel array is larger, or smaller, than the image size.(We won't be using this error in our use case).
Now, Rust allows us to easily implement a function to throw an error whenever there is one with working with files.
In Rust, the keyword
impl is used to implement functions for a struct. There is also
trait, which stores functions that you can later implement for a specific(or multiple) structs.
The syntax of implementing a trait for a struct is
impl Trait for Struct. Simple!
Rust has a trait
From<T> that allows us to implement the function
from. The trait
From<T> is widely used in implementing io errors.
io::Error is the "generic" type. An
io::Error is an error that occurs anytime throughout working with files(in our case). As stated above, the trait
From<T> gives us the function
from. This function allows us to automatically throw a specific error(in our case
FileError whenever there is, of course, an IO error.
Now, lets define some of our own traits to later implement for each individual struct.
I like to add in "Funcs" at the end of the trait name. It just helps me notice that this is a trait for the specific struct.
The first trait we'll create is for the
BmpImageInfo struct. We're going to want a few functions:
- A function that creates a new instance of everything within the struct
- A function that enables us to gather the pixel array, and assign it accordingly
- A function to create the bmp image.
new_bmp takes in arguments
width so we have some standard common ground to start working with. This also enables us to alter calculate the image size.
Next, lets create a trait for the
BmpHeader struct. This trait, too, will need a few functions:
- A function that creates a new instance of the struct
- A function that assigned values, accordingly, to the structs variables
Now, we need a trait for
BmpDibHeader. This trait, too, will have a few functions:
- A function to create a new instance of the struct
- A function to assign values, accordingly, to the structs variables
Now, we need some traits for the errors. A trait for the struct
ErrInfo that contains information about the error, and a trait for the other errors we defined in
ErrInfo just needs a single functions that assigns values to its variables.
BmpImageErrs enum just needs a few functions to create a new instance of each error.
Now, all together, you should have the following code:
Sweet! We got the "header" file down that contains the "outline" of the actual Rust code. Now, lets start writing some REAL Rust code, and see where we end up!
First before all, and I apologize for not mentioning, I have isolated the main code in another folder, preferably another /src folder. You can do whatever you want, but in my case, I will be needing a
This file will enable us to use this Rust code anywhere else in this /src folder. It's a real simply Rust file, we simply just create a
mod of the Rust code.
Also note, the
pub keyword is required if you wish to use any of this Rust code in another file. So all variables within a struct need to be public, even the structs themselves. However, traits do not because they tend to be implied, and if they're implied then there is no use case to needing to directly tell the compiler that they're public.
While we are on the topic of the file
mod.rs, lets quickly add in this new file. I named it
imprender.rs, because we are implementing the functionality for all the code in
render.rs. You can name it however you want, now, lets add this to mod.rs:
Now, Rust is a bit strict over snake casing. So, to avoid all the annoying warnings that the Rust compiler generates over snake casing, I added in a simple line at the top of
imrender.rs that will simply tell the Rust compiler, "Hey, don't blow me up with all these errors".
In Rust, whenever you see a
#, you are about to see an attribute. Each attribute has it's own meaning, along with its own parameters.
allow is available to us to, well, allow specific things that would rather trigger a warning.
We can use
#! to tell the Rust compiler that this specific attribute is standard throughout the whole file.
Lets go ahead and tell the Rust compiler to not trigger warnings over any snake casing:
Pfft. That was simple. Onto the next thing!
In Rust, there is this keyword
super. Primarily what it does is enable us to access a module from within the directory. The file
mod.rs enables us to sufficiently use this
super keyword to use and distribute code from another file within this specific file.
Now, don't get confused when I say the word "mod"(module). A module in Rust is just a simple way of organizing the code.
Since we have
pub mod render inside
mod.rs, it is safe to assume that we can thus use the
super keyword to gain access to using this file within
There is a catch to this, however. Just because we have
super::render, doesn't mean we can spontaneously use all the structs/traits/enums within the file, directly.
Keyword, directly. With just having
use super::render, this means we have to vividly tell the compiler where the struct is located. We can't just go
BmpInfo, and the Rust Compiler knows right away.
So, how can we fix this? Simple! We just tell the Rust Compiler what we want to use from that file, via the same syntax!
In the code above, we tell the Rust Compiler we want to have access to all the code within the file(or module)
render.rs. The next few lines explicitly tells the Rust Compiler what we want to use directly from this file. Notice that we don't implement the
super keyword before each
use case. Why is that? Well, the
super keyword is used to gain access to code from another file(or module). Since we already have
use super::render, there is no need to keep using the
Now, we need some other things. We need something to enable us to create a file, we need something to enable us to write to the file, and something to allow us to check if the file already exists.
std::fs::File will give us access to a range of functions, from opening and reading a file, to creating a file.
std::io::Write allows us to be capable of writing to a file.
std::path::PathBuf allows us to work with paths, in our case, to make sure they either exist or not.
Now, I think it's safe to say that errors are really important. So, first things first, lets implement the trait
ErrInfoFuncs for the struct
In Rust, you don't need the
return keyword. In the code below, you will notice a few things:
- The return type is
- There is no
Normally in small functions like the one below, Rust doesn't require you to have a return statement. When it comes to longer functions it tends to become better to use return statements.
The return type of
Self just tells the Rust Compiler to expect a return type of what we are implementing the trait for, in our case, the
I think that is pretty simple. It is good practice to put the name of the variable before the value, so that, when referencing this code later on, it is much easier to know what each value is, and what it's meaning is.
Now, lets implement the
BmpImageErrsFuncs for the enum
This is some simple stuff. It is just a function that takes in some information, then returns accordingly to that specific function call.
ErrCreating takes in the
ErrInfo struct. Hence as to why we implemented the trait for the struct
Now, lets move onto the more exciting things. Lets start implementing the trait for the
The init function
new_header is pretty easy to grasp your head around. It initializes default values for the
header_field won't be mutated throughout the program, so we assign the default value it needs.
Lets now move onto the
assign function. The
assign function will be used to assign values within the struct.
Notice, I used the
return keyword, along with returning a "copy" of the struct, to stay prone of any errors.
Now, lets start implementing the trait
BmpDIBHEADER struct. Same as the last one, the
new_dib_header will just assign initial values for all the variables within the struct.
compression will all stay as there initial values.
assign_dib_header is the same as the
assign function for the struct
BmpHEADER, it will be used to reassign values within the struct.
Notice, I use, yet again, the
return keyword along with creating a "copy" of the struct to stay prone of errors.
Now, we're getting into the fun stuff. This next implementation is what will allow us to create this BMP image.
Firstly, as always, the
new_bmp function assigns initial values to the structs variables.
There is a reason I organized the code the way I did. I put all the other implementations above this specific one since this struct is using the other structs.
Next, we have the
configure_bmp function. This function simply just sets up some basic information for the BMP image. It also gets the pixel array to be used to actually have the image.
In this function, we use the
assign function that was implemented for
BmpHEADER to sufficiently assign the values, prone to errors.
I haven't quite gone over the
Result<T, T> yet. The return type of
Result<T, T> is Rusts way of handling errors. If there is an error that occurs within a function, instead of the compiler worrying about it, it is rather up to the developer to deal with it.
Return<t1, t2> - t1 is the return type if the function succeeds without an error, t2 is the return type if the function fails.
In our case, with
configure_bmp, if the function succeeds, we return
BmpImageInfo struct, if it fails, we return an error from
To tell the Rust compiler what the status of the return is, if it's a failure you use
Err, if it is a success you use
Ok. Notice, yet again, that we are creating a "copy" of the struct to stay prone of errors.
Now, the next function is where the magic happens. This function will take information from the two other structs and convert it into the 2 arrays(
These two arrays are Vectors. Vectors are dynamic arrays in Rust. With Vectors, you can push, remove, clear and do so much more than with the standard built-in static arrays.
Vectors can only store one type of data. In our case, we just want to store
u8 data types(since we're primarily writing binary).
Now, since we are working with the type
u8, we have to physically put in the exceeding bytes to make sure we're writing 4-byte buffers to the file(disregarding the 2-byte header field, and the pixel array).
We define the variable
f as mutable so we can write to the file. The
File::create allows us to create a new file. We do check if the file currently exists before attempting to create it, with
file is of the type
You might be wondering, why on earth do I have question marks after some of those lines of code. What this question marks do is rather simplify our code. Instead of needing to write a match statement for each operation, this question mark simplifies things down to automatically throw an error. This is where the implementation of the
From<io::Error> comes in. That question mark will automatically throw an IO error, if any, and since we have implemented that one of our errors be returned in an instance of an IO error, we will thus get a
Lets see if it works!
When you ran
cargo init, it should of given you a
main.rs file under /src. Within
main.rs, lets attempt to create a 2x2 BMP image, using the functionality we have just created.
So, first before all, lets go ahead and tell the Rust compiler to not give us any warnings over snake case.
Now, we are going to need to tell the Rust compiler what we need to be capable of rendering this BMP image.
mod.rs is located in the /src folder, which, too, is located in the /src folder created by cargo.
mod.rs allows us to then use the module of the whole folder. We simply do this by saying
Then, we will use the module to tell the Rust compiler what files we want to work with. It's the same as the super keyword, only in this case we have to explicitly use the
mod keyword due to the fact we are no longer within the folder.
Good. Now, we can go ahead and continue on with the code.
First things first, lets initialize the
BmpImageInfo struct. We have to declare this variable as mutable so we can thus call the functions associated with the implementation of the trait to the struct.
If you recall, the
new_bmp function takes in a height and width as its arguments. We are wanting a 2x2 BMP image, so we pass in these parameters accordingly.
Now, the function
configure_bmp returns a
Result<T1, T2>, so this is going to have to be a match statement.
I believe we could simply use the question mark, and perhaps you could try this, but I am sticking with the match statement for simplicity.
And with that, I think this tutorial is completed!
You know have some basic insight on how to render a BMP image using Rust. I took a more broad and thought through approach with how I laid out my code, and organized it. You can, however, do this in any way you want.
BMP image rendering is a bit more lengthy using Rust due to Rust having more code to be written to allow us to have some functionality. But, without a doubt, it was a fun experience writing this program, and it was a fun experience writing this tutorial.
If you have any question, don't hesitate to ask!
Until the next tutorial, MocaCDeveloper, out!