first things first
i need a way to render pixels 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 run the command inside rust when i generate a new image. Sdl2 has 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();
}to print a line of pixels, a couple considerations. for computers to read a bunch of bytes we need to provide a specific encoding. since we're starting with png the rust community provides a crate that helps. images are encoded in different ways and the headers explain how to read the file. we need to decide the color type beforehand. RGB for now. 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 won't work yet because it's missing an image. think of an image as an infinite line of contiguous bytes (in this case 8bit bytes). group that line of bytes in groups of three and you get an RGB pixel. so the image needs to be an N vector divisible by 3. a 3 element array is a single pixel. a line of 10 pixels is a 30 element array.
let pixel = vec![255;3];
let line = vec![255;30];printing that gives a white line. the width and height passed to the encoder represent the size of the canvas of bytes the image fills. passing more or fewer bytes fails the write.
a struct to handle the RGB logic lets us manipulate vectors of colors instead of thinking in an infinite line. images become a grid of pixels.
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,
}
}
}a utility function turns our Vec<Rgb> into Vec<u8>.
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();
}