diff --git a/.gitignore b/.gitignore index 641e562..a810fde 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,3 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk -# testing files -image.png diff --git a/images/anime.jpg b/images/anime.jpg new file mode 100644 index 0000000..5d88110 Binary files /dev/null and b/images/anime.jpg differ diff --git a/images/anime_2.jpg b/images/anime_2.jpg new file mode 100644 index 0000000..623b1a0 Binary files /dev/null and b/images/anime_2.jpg differ diff --git a/images/image.png b/images/image.png new file mode 100644 index 0000000..7d94e69 Binary files /dev/null and b/images/image.png differ diff --git a/images/money_mouth_face.png b/images/money_mouth_face.png new file mode 100644 index 0000000..47927b9 Binary files /dev/null and b/images/money_mouth_face.png differ diff --git a/src/args.rs b/src/args.rs index 714f13a..2a16eb8 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,5 +1,6 @@ pub mod args { - use clap::{Parser, arg, ValueEnum, ColorChoice}; + use clap::{Parser, arg, ColorChoice}; + use super::enums::*; #[derive(Parser, Debug)] #[command(author, version, about, long_about=None, color=ColorChoice::Always)] @@ -13,14 +14,38 @@ pub mod args { pub image: String, /// The character to use for drawing the image (lighter to darker) /// You can user one character if you uses the color mode - #[arg(short, long, default_value=" .,-~:;=!*#$@")] + #[arg(short, long, default_value=" .,-~!;:=*&%$@#")] pub characters: String, + /// The output scale (1 is the original size) + #[arg(short, long, default_value="4")] + pub scale: u32, /// The output file to write to (if output_method is file) #[arg(short, long, default_value="ascii_image.txt")] pub output: String, } - #[derive(Copy, Clone, ValueEnum, Debug, PartialOrd, Eq, PartialEq)] + impl Arguments { + pub fn validate(&self) -> Result<(), String> { + if self.characters.len() == 0 { + return Err("No characters provided".to_string()); + } else if self.characters.len() == 1 { + if self.mode == Mode::NormalAscii { + return Err("One character provided but mode is normal-ascii".to_string()); + } + } else if self.characters.len() > 32 { + return Err("Too many characters provided, max is 32".to_string()); + } + Ok(()) + } + } + + +} + +pub mod enums { + use clap::ValueEnum; + + #[derive(Copy, Clone, ValueEnum, Debug, PartialOrd, Eq, PartialEq)] pub enum Mode { /// Normal ASCII art #[clap(alias = "n")] @@ -35,9 +60,6 @@ pub mod args { /// Save the ascii art to a file #[clap(alias = "f")] File, - #[clap(alias = "c")] - /// Copy the ascii art to the clipboard - Clipboard, /// Print the ascii art to the terminal #[clap(alias = "s")] Stdout, diff --git a/src/ascii_processor.rs b/src/ascii_processor.rs new file mode 100644 index 0000000..e5467e2 --- /dev/null +++ b/src/ascii_processor.rs @@ -0,0 +1,74 @@ +use clap::arg; +use image::{GenericImageView, DynamicImage}; +use colored::{ColoredString, Colorize}; +use crate::args::{ + args::Arguments, + enums::Mode +}; + +pub fn generate_ascii(image: DynamicImage, args: &Arguments) -> Result, error::ASCIIProcessingError> { + let characters = args.characters.chars().collect::>(); + trace!("Characters: {:?}, length: {}", characters, characters.len()); + let mut output = Vec::new(); + let (width, height) = image.dimensions(); + + for y in 0..height { + for x in 0..width { + if y % (args.scale * 2) == 0 && x % args.scale == 0 { + output.push(get_character(image.get_pixel(x, y), &characters, args.mode)); + } + } + // Add a new line at the end of each row + if y % (args.scale * 2) == 0 { + output.push("\n".into()); + } + } + + Ok(output) +} + +fn get_character(pixel: image::Rgba, characters: &Vec, mode: Mode) -> ColoredString { + let intent = if pixel[3] == 0 { 0 } else { pixel[0] / 3 + pixel[1] / 3 + pixel[2] / 3 }; + + let ch = characters[(intent / (32 + 7 - (7 + (characters.len() - 7)) as u8)) as usize]; + + let ch = String::from(ch); + + match mode { + Mode::NormalAscii => ColoredString::from(&*ch), + Mode::COLORED => { + ch.to_string() + .truecolor(pixel[0], pixel[1], pixel[2]) + } + } +} + +mod error { + use std::error::Error; + use std::fmt::{Debug, Display, Formatter}; + + #[derive(Debug)] + pub struct ASCIIProcessingError { + message: String, + } + + impl ASCIIProcessingError { + pub fn new(message: String) -> Self { + ASCIIProcessingError { + message + } + } + } + + impl Display for ASCIIProcessingError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } + } + + impl Error for ASCIIProcessingError { + fn description(&self) -> &str { + &self.message + } + } +} diff --git a/src/main.rs b/src/main.rs index 8610c5d..ddba124 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,21 @@ +use std::io::Write; use clap::Parser; use image::GenericImageView; +use colored::Colorize; extern crate pretty_env_logger; -#[macro_use] extern crate log; +#[macro_use] +extern crate log; mod args; +mod ascii_processor; -use crate::args::args::Arguments; +use crate::args::{ + args::Arguments, + enums::{Mode, OutputMethod}, +}; + +use crate::ascii_processor::generate_ascii; fn main() { // Initialize the logger @@ -18,9 +27,20 @@ fn main() { info!("Successfully parsed arguments"); trace!("Arguments: {:?}", arguments); + // Validate the arguments + info!("Validating arguments"); + match arguments.validate() { + Ok(_) => (), + Err(e) => { + error!("Failed to validate arguments: {}", e); + eprintln!("Failed to validate arguments: {}", e); + std::process::exit(1); + } + } + // Open the image info!("Opening image: {}", arguments.image); - let image = match image::open(arguments.image) { + let image = match image::open(arguments.image.clone()) { Ok(image) => image, Err(e) => { error!("Failed to open image: {:?}", e); @@ -31,4 +51,39 @@ fn main() { info!("Successfully opened image"); trace!("Image dimensions: {:?}", image.dimensions()); + // Process the image + let output = match generate_ascii(image, &arguments) { + Ok(out) => { + info!("Successfully processed image"); + out + } + Err(e) => { + error!("Failed to process image: {:?}", e); + eprintln!("Failed to process image: {:?}", e); + std::process::exit(1); + } + }; + + // Output the image + info!("Outputting image"); + match arguments.output_method { + OutputMethod::File => { + match std::fs::write(arguments.output.clone(), + output.iter().map(|s| s.to_string()).collect::()) { + Ok(_) => info!("Successfully outputted image: {}", arguments.output), + Err(e) => { + error!("Failed to output image: {:?}", e); + eprintln!("Failed to output image: {:?}", e); + std::process::exit(1); + } + } + } + OutputMethod::Stdout => { + for char in output { + print!("{}", char); + std::io::stdout().flush().unwrap(); + } + info!("Successfully outputted image"); + } + } }