diff --git a/.gitignore b/.gitignore index 088ba6b..9724902 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,9 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +#intellij folder +.idea/ + +#test files +*.png diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..11fd512 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "img2ascii" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +env_logger = "0.10.*" +dotenvy = "0.15.*" +log = "0.4.*" +clap = { version = "4.1.*", features = ["derive"] } +image = "0.24.*" +termion = "2.0.*" \ No newline at end of file diff --git a/README.md b/README.md index e73bc63..daa1849 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # img2ascii CLI that converts images into ascii art and prints them to the console + + + +//todo: Update this \ No newline at end of file diff --git a/src/ascii_manipulation.rs b/src/ascii_manipulation.rs new file mode 100644 index 0000000..4d0ff25 --- /dev/null +++ b/src/ascii_manipulation.rs @@ -0,0 +1,44 @@ +use image::{DynamicImage, GenericImageView, Rgba}; + +//todo: consider how to take care of the a channel => do we want to render that as background? +fn get_color(pixel: (u32, u32, Rgba)) -> u8 { + let vec = pixel.2.0; + //luminosity method of getting lightness + (vec[0] as f32 * 0.3 + vec[1] as f32 * 0.59 + vec[2] as f32 * 0.11) as u8 +} + +fn to_ascii(char_map: String, image: DynamicImage) -> Vec { + //todo: add color support + let l = char_map.len() as f32; + let mut str = String::new(); + let mut out: Vec = Vec::new(); + + for pixel in image.pixels() { + let ch = char_map.as_bytes()[((get_color(pixel) as f32-1.0)/255f32 * l) as usize]; //fixme: might break with non-ASCII char_map (ie braille chars, possibly) + str.push(char::from(ch)); //fixme: this is finicky and also very unsafe + + if pixel.0 == image.width()-1 { + out.push(str); + str = String::new(); + } + } + out +} + +pub fn to_simple_ascii(image: DynamicImage) -> Vec { + to_ascii(" .:-=+*#%@".to_owned(), image) +} + +pub fn to_complex_ascii(image: DynamicImage) -> Vec { + to_ascii(" .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$".to_owned(), image) +} + +pub fn to_braille_ascii(image: DynamicImage) -> Vec { + //todo: figure out braille symbols + vec!["not implemented".to_owned()] +} + +pub fn to_custom_ascii(char_map: String, image: DynamicImage) -> Vec { + //todo: this + vec!["not implemented".to_owned()] +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..0b6cff5 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,113 @@ +use std::ffi::OsString; +use std::path::PathBuf; +use std::process::exit; +use clap::Parser; +use log::{debug, error}; +use image; + +/// Convert an image to ASCII art and print it to the terminal. +/// +/// Configuration is done using flags. +#[derive(Parser)] +pub struct Cli { + /// Use a larger range of characters + #[arg(short = 'c', long)] + pub complex: bool, + /// Display ASCII art in full colour + #[arg(short = 'C', long)] + pub colour: bool, + /// Use braille characters instead of ASCII + #[arg(short = 'b', long)] + pub braille: bool, + /// Print debugging information + #[arg(short = 'd', long)] + pub debug: bool, + /// Image path + pub image: PathBuf, + #[arg(short = 'w', long)] + /// Set the width of the output, instead of using terminal width + pub width: Option, + /// Save the output to a file, instead of printing to terminal + #[arg(short = 'o', long = "output")] + pub output: Option, +} + + +impl Cli { + pub fn debug_print(&self) { + debug!("complex: {}", self.complex); + debug!("colour: {}", self.colour); + debug!("braille: {}", self.braille); + debug!("debug: {}", self.debug); + debug!("width: {}", self.width.unwrap_or(u16::MAX)); + debug!("image: {}", self.image.display()); + if let Some(output) = self.output.clone() { + debug!("output: {}", output.display()); + } else { + debug!("output: None"); + } + } + + pub fn validate(&self) { + if !file_exists(self.image.clone()) { + error!("Input file \"{}\" does not exist!", self.image.display()); + exit(1); + } + + if !is_image(self.image.clone()) { + error!("Input file \"{}\" is not an image!", self.image.display()); + exit(1); + } + + if let Some(output) = self.output.clone() { + if file_exists(output.clone()) { + error!("Output file \"{}\" already exists!", output.display()); + exit(1); + } + } + } + + pub fn init(&self) { + if self.debug {std::env::set_var("RUST_LOG", "trace")} + env_logger::init(); + self.debug_print(); + } +} + +/// This function checks if a given file is an image +/// +/// arguments: +/// path: PathBuf - path to the file to check +/// +/// returns: +/// boolean indicating if file is an image or not +fn is_image(path: PathBuf) -> bool { + let ext = path.extension(); + match ext { + Some(ext) => { + vec![OsString::from("png"), + OsString::from("jpg"), + OsString::from("jpeg"), + OsString::from("webp")] + .contains(&ext.to_ascii_lowercase()) + } + None => false + } +} + +/// This function checks if a given file exists +/// +/// arguments: +/// path: PathBuf - path to the file to check +/// +/// returns: +/// boolean indicating if the file exists or not +fn file_exists(path: PathBuf) -> bool { + match path.try_exists() { + Ok(bool) => bool, + Err(e) => { + error!("{}", e.to_string()); + false + } + } +} \ No newline at end of file diff --git a/src/image_manipulation.rs b/src/image_manipulation.rs new file mode 100644 index 0000000..0a9d4de --- /dev/null +++ b/src/image_manipulation.rs @@ -0,0 +1,38 @@ +use std::cmp::min; +use termion::terminal_size; +use log::error; +use std::process::exit; +use std::path::PathBuf; +use image::DynamicImage; + +fn get_terminal_size() -> u32 { + let size = terminal_size(); + match size { + Ok(size) => { + size.0 as u32 + } + Err(e) => { + error!("Failed to get terminal size: {}", e.to_string()); + exit(1); + } + } +} + +pub fn get_size(w: Option, img_w: u32) -> u16 { + if None == w { + min(get_terminal_size(), img_w) as u16 + } else { + w.unwrap() + } +} + +pub fn open_image(path: PathBuf) -> DynamicImage { + let img = image::open(path); + match img { + Ok(img) => img, + Err(e) => { + error!("Failed to open image: {}", e.to_string()); + exit(1) + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..5c9d089 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,41 @@ +use clap::Parser; +use image::imageops::FilterType; +use crate::ascii_manipulation::to_simple_ascii; +use crate::cli::Cli; +use crate::output::print_terminal; + +mod cli; +mod image_manipulation; +mod ascii_manipulation; +mod output; + +//todo: general +/* https://stackoverflow.com/questions/69981449/how-do-i-print-colored-text-to-the-terminal-in-rust + */ + +fn main() { + std::env::set_var("RUST_LOG", "info"); + + //parse flags + let cli: Cli = Cli::parse(); + cli.init(); + cli.validate(); + + let mut img = image_manipulation::open_image(cli.image); + let w = image_manipulation::get_size(cli.width, img.width()); + + //todo: change logic to include -full flag for max width, otherwise use max height? + let h: u32 = (img.height() as f32 * w as f32 / img.width() as f32 * 0.5) as u32; + img = img.resize_exact(w as u32, h, FilterType::CatmullRom); + + let out = to_simple_ascii(img); + + print_terminal(out); + + //todo: + /* function that converts image to braille (if -b) + * something about printing in colour + * a function to let the user define a custom map + */ + +} diff --git a/src/output.rs b/src/output.rs new file mode 100644 index 0000000..2b74c41 --- /dev/null +++ b/src/output.rs @@ -0,0 +1,11 @@ +use std::path::PathBuf; + +pub fn print_terminal(ascii: Vec) { + for line in ascii { + println!("{}", line); + } +} + +pub fn print_file(ascii: Vec, out: PathBuf) { + //todo: output +} \ No newline at end of file