ASCII Art in Rust 🦀

May 1, 2024

ASCII Art in Rust 🦀

In this post, we will be creating a Rust program that converts an image to ASCII art and prints that in terminal (in colour aswell).

Prerequisites

Before we start, make sure you have Rust installed on your system. If not, you can install it by following the instructions on the Official Rust website.

Setting up the project

Create a new Rust project using the following command:

$ cargo new ascii_art $ cd ascii_art

We will use the image crate to read the image and colored crate to print the ASCII art in colour. Add these dependencies to your Cargo.toml file:

# Cargo.toml [dependencies] image = "0.25.1" colored = "2.1.0"

Now, let's write the code to convert the image to ASCII art.

Writing the code

Delete the contents of the src/main.rs file and add the following code. This code imports the required crates:

// src/main.rs use image::GenericImageView; use colored::Colorize;

Now, let's write the function to convert the image to ASCII art. We will use the image::open function to open the image and image::GenericImageView trait to get the dimensions of the image. Don't forget to add your image file in the project directory where Cargo.toml is located.

get_image function

// src/main.rs fn get_image(dir: &str) { let img = image::open(dir).expect("File not found"); println!("{:?}", img.dimensions()); } fn main() { get_image("image.jpg"); }
(1000,1000)

As you can see, the get_image function prints the dimensions of the image. Now, let's convert the image to ASCII art. This will be a bit complex, so I will explain the code in parts next.

// src/main.rs // in get_image function let (width, height) = img.dimensions(); for y in 0..height { for x in 0..width { if y % (scale * 2) == 0 && x % scale == 0 { let pix = img.get_pixel(x, y); let mut intent = ((pix[0] as u32 + pix[1] as u32 + pix[2] as u32) / 3) as u8; if pix[3] == 0 { intent = 0; } print!("{}", get_str_ascii(intent, negative).truecolor(pix[0], pix[1], pix[2])); } } if y % (scale * 2) == 0 { println!(""); } }

Lets start from the top.

The code below gets the dimensions of the image and stores them in the width and height variables.

let (width, height) = img.dimensions();

The code below loops through the height of the image then the width of the image. You can see we have y as the height and x as the width because MATHS 😂.

for y in 0..height { for x in 0..width { } }

The code below checks if the current y is divisible by scale * 2 and x is divisible by scale. We do this to reduce the number of pixels we print to the terminal else the image will be too large for high-resolution image. We will talk about the scale variable later.

Note: This is because the height of a character is more than the width of a character in terminal. So by multiplying the scale by 2, we get a more accurate representation of the image.

if y % (scale * 2) == 0 && x % scale == 0 { }

The code below gets the pixel at the current x and y position. The get_pixel function returns a Rgba struct which contains the pixel data.

let pix = img.get_pixel(x, y);

The code below calculates the intensity of the pixel by averaging the red, green, and blue values. We divide by 3 because we are converting the pixel to grayscale.

Also, we cast the result to u8 because the truecolor function only accepts u8 values.

Note: u8 is an unsigned 8-bit integer which ranges from 0 to 255.

let mut intent = ((pix[0] as u32 + pix[1] as u32 + pix[2] as u32) / 3) as u8;

Optional: You can use the formula below to get a more detailed intensity value.

let mut intent = (0.299 * pix[0] as f32 + 0.587 * pix[1] as f32 + 0.114 * pix[2] as f32) as u8;

The code below checks if the alpha value of the pixel is 0. If the alpha value is 0, it means the pixel is transparent, so we set the intensity to 0.

if pix[3] == 0 { intent = 0; }

The code below prints the ASCII character based on the intensity of the pixel. We use the get_str_ascii function to get the ASCII character based on the intensity. We also use the truecolor function to print the ASCII character in colour.

print!("{}", get_str_ascii(intent, negative).truecolor(pix[0], pix[1], pix[2]));

Now let's code the get_str_ascii function.

get_str_ascii function

// src/main.rs fn get_str_ascii(intent: u8, neg: bool) -> &'static str { let index = intent / 32; let mut ascii = [" ", ".", ",", "-", "~", "+", "=", "@"]; if neg == true { ascii.reverse(); } ascii[index as usize] }

Let's break down the code.

The code below calculates the index of the ASCII character based on the intensity of the pixel. We divide the intensity by 32 because we have 8 ASCII characters and 256 intensity levels (0 -> 255).

let index = intent / 32;

The code below creates an array of ASCII characters. We use the index to get the ASCII character from the array.

Note: The ASCII characters are arranged from light to dark.

let mut ascii = [" ", ".", ",", "-", "~", "+", "=", "@"];

The code below checks if the neg parameter is true. If it is true, it reverses the array to give the negative effect.

if neg == true { ascii.reverse(); }

The code below returns the ASCII character based on the index.

Note: We cast the index to usize because the array index must be an unsigned integer. usize is an unsigned integer that is the same size as a pointer.

ascii[index as usize]

Now, the code is complete. You can run the program by calling the get_image function in the main function.

When calling the get_image function, pass the path to the image file as an argument.

Note: We use & before the file path to pass a reference to the string.

fn main() { let file = "image.jpg"; let scale = 7; get_image(&file, scale, false); }

Note: If your image is too large, you can increase the scale value to reduce the number of pixels printed to the terminal.

Running the program

To run the program, use the following command:

$ cargo run

You should see the ASCII art of the image printed in the terminal.

What's next?

You can add more features to the program and make it more interactive. Like adding a command-line argument to pass the image file path like in the code below.

fn main() { let file = std::env::args().nth(1).expect("No file mentioned"); let scale: u32 = std::env::args().nth(2).expect("Scale not mentioned").parse().unwrap(); get_image(&file, scale, false); }
$ cargo run image.jpg 7

Conclusion

In this post, we created a Rust program that converts an image to ASCII art. We used the image crate to read the image and colored crate to print the ASCII art in colour.

This is the full code of the program:

use colored::Colorize; use image::GenericImageView; fn get_str_ascii(intent: u8, neg: bool) -> &'static str { let index = intent / 32; let mut ascii = [" ", ".", ",", "-", "~", "+", "=", "@"]; if neg == true { ascii.reverse(); } ascii[index as usize] } fn get_image(dir: &str, scale: u32, negative: bool) { let img = image::open(dir).expect("File not found"); println!("{:?}", img.dimensions()); let (width, height) = img.dimensions(); for y in 0..height { for x in 0..width { if y % (scale * 2) == 0 && x % scale == 0 { let pix = img.get_pixel(x, y); let mut intent = ((pix[0] as u32 + pix[1] as u32 + pix[2] as u32) / 3) as u8; if pix[3] == 0 { intent = 0; } print!( "{}", get_str_ascii(intent, negative).truecolor(pix[0], pix[1], pix[2]) ); } } if y % (scale * 2) == 0 { println!(""); } } } fn main() { let file = std::env::args().nth(1).expect("No file mentioned"); let scale: u32 = std::env::args().nth(2).expect("Scale not mentioned").parse().unwrap(); get_image(&file, scale, false); }

Thanks for reading! I hope you enjoyed this post. More posts are coming soon. Stay tuned!

If you found any mistake, then dm me on Instagram 😂