Processing images in rust

Building a simple http server in rust

First things first

I need to find a way to render pixeles on the terminal to test what I'm doing, so after some searching and experimentation I settled for the worst solution. But it's simple enough. I installed viu and I run the command inside rust when I generate a new image. At some point I'll dive deep into how does it actually achive that. Sld2 seems to have some issues linking on my m1 so that's going to have to do at another time

use std::{process::Command, fs::File, io::BufWriter};
 
fn main() {
  let path = std::path::PathBuf::from("some/img/path.png");
 
  Command::new("viu").arg(path).spawn().unwrap();
}

Now I need to be able to print a line of pixels, for that we are going to need to make a couple considerations, in order for computers to read bunch of bytes we need to provide an specific encoding. Because we are going to start with png the rust community kindle provides a crate that should help us out. Images are encoded in different ways and the headers explain sort of how to read the file. We need to decide before hand the color type we are going to keep it simple with RGB for now; and we also need to provide the BitDepth which is 8bit.

let width = 30;
let height = 1;
let file = File::create(&path).unwrap();
let ref mut w = BufWriter::new(file);
let mut encoder = Encoder::new(w, width as u32, height);
encoder.set_color(ColorType::Rgb);
encoder.set_depth(BitDepth::Eight);
 
let mut writer = encoder.write_header().unwrap();
 
match writer.write_image_data(&image) {
  Ok(_) => println!("Image written successfully"),
    Err(e) => println!("Error writing image: {}", e),
}

This code will not work yet because it's missing an image. You can imaging an image like an infinite line of contiguos bytes (in this case 8bit bytes). Now if we group that line of bytes in groups of three we get an RGB pixel. So the only thing our image needs is to be an N vector dividable by 3. So we can imagine that a 3 elements array would be the same as a single pixel. And a line of 10 pixels would be a 30 elements array.

let pixel = vec![255;3];
let line = vec![255;30];

If we try to print that we will get a fine white line. Take into consideration that the width and height you are passing to the encoder will represent the size of the canvas of bytes the image is expected to fill. If you pass more or less bytes then the writing of the image will fail.

To make things more ergonomic lets create some struct to handle the RGB logic so we can manipulate vectors of colors instead of thinking in a infinite line. This will make it easier to think of images as a grid of pixeles.

rgb.rs

#[derive(Debug, Clone, Copy)]
pub struct Rgb {
    pub r: f32,
    pub g: f32,
    pub b: f32,
}
 
impl Rgb {
    pub fn new(r: f32, g: f32, b: f32) -> Rgb {
        Rgb { r, g, b }
    }
 
    pub fn black() -> Rgb {
        Rgb { r: 0.0, g: 0.0, b: 0.0 }
    }
 
    pub fn white() -> Rgb {
        Rgb { r: 1.0, g: 1.0, b: 1.0 }
    }
 
    pub fn to_u8(&self) -> Vec<u8> {
        vec![
            (255.0 * self.r) as u8,
            (255.0 * self.g) as u8,
            (255.0 * self.b) as u8
        ]
    }
 
    pub fn invert(color: &Rgb) -> Rgb {
        Rgb {
            r: 1.0 - color.r,
            g: 1.0 - color.g,
            b: 1.0 - color.b,
        }
    }
}

I introduced a utility function to turn our Vec<Rgb> into Vec<u8>. This will allow us to organize the code easier and make it more readable

Final code

main.rs

 
pub mod rgb;
 
use std::{process::Command, fs::File, io::BufWriter};
use png::{Encoder, ColorType, BitDepth};
use rgb::Rgb;
use ndarray::{Array1, ArrayView, Axis};
 
fn main() {
    let path = std::path::PathBuf::from("out.png");
    let width = 100;
    let height = 1;
 
    let mut line = Array1::from(vec![]);
    let binding = [Rgb::white(); 20];
    let ones  = ArrayView::from(&binding);
    let binding = [Rgb::black(); 40];
    let zeros = ArrayView::from(&binding);
    line.append(Axis(0), zeros).unwrap();
    line.append(Axis(0), ones).unwrap();
    line.append(Axis(0), zeros).unwrap();
 
 
    let image: Vec<u8> = line.iter().flat_map(|x| x.to_u8()).collect();
 
    println!("{:?}\n", image);
 
    let file = File::create(&path).unwrap();
    let ref mut w = BufWriter::new(file);
    let mut encoder = Encoder::new(w, width as u32, height);
    encoder.set_color(ColorType::Rgb);
    encoder.set_depth(BitDepth::Eight);
 
    let mut writer = encoder.write_header().unwrap();
 
    match writer.write_image_data(&image) {
        Ok(_) => println!("Image written successfully"),
        Err(e) => println!("Error writing image: {}", e),
    }
 
    Command::new("viu").arg(path).spawn().unwrap();
}