Create the ascii_processor 🥰🦀

This commit is contained in:
Anas Elgarhy 2022-10-03 20:13:59 +02:00
parent 0ac98a32ac
commit b885cc3fb7
8 changed files with 160 additions and 11 deletions

2
.gitignore vendored
View file

@ -11,5 +11,3 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
# testing files
image.png

BIN
images/anime.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
images/anime_2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
images/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

BIN
images/money_mouth_face.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,5 +1,6 @@
pub mod args { pub mod args {
use clap::{Parser, arg, ValueEnum, ColorChoice}; use clap::{Parser, arg, ColorChoice};
use super::enums::*;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(author, version, about, long_about=None, color=ColorChoice::Always)] #[command(author, version, about, long_about=None, color=ColorChoice::Always)]
@ -13,13 +14,37 @@ pub mod args {
pub image: String, pub image: String,
/// The character to use for drawing the image (lighter to darker) /// The character to use for drawing the image (lighter to darker)
/// You can user one character if you uses the color mode /// You can user one character if you uses the color mode
#[arg(short, long, default_value=" .,-~:;=!*#$@")] #[arg(short, long, default_value=" .,-~!;:=*&%$@#")]
pub characters: String, 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) /// The output file to write to (if output_method is file)
#[arg(short, long, default_value="ascii_image.txt")] #[arg(short, long, default_value="ascii_image.txt")]
pub output: String, pub output: String,
} }
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)] #[derive(Copy, Clone, ValueEnum, Debug, PartialOrd, Eq, PartialEq)]
pub enum Mode { pub enum Mode {
/// Normal ASCII art /// Normal ASCII art
@ -35,9 +60,6 @@ pub mod args {
/// Save the ascii art to a file /// Save the ascii art to a file
#[clap(alias = "f")] #[clap(alias = "f")]
File, File,
#[clap(alias = "c")]
/// Copy the ascii art to the clipboard
Clipboard,
/// Print the ascii art to the terminal /// Print the ascii art to the terminal
#[clap(alias = "s")] #[clap(alias = "s")]
Stdout, Stdout,

74
src/ascii_processor.rs Normal file
View file

@ -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<Vec<ColoredString>, error::ASCIIProcessingError> {
let characters = args.characters.chars().collect::<Vec<char>>();
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<u8>, characters: &Vec<char>, 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
}
}
}

View file

@ -1,12 +1,21 @@
use std::io::Write;
use clap::Parser; use clap::Parser;
use image::GenericImageView; use image::GenericImageView;
use colored::Colorize;
extern crate pretty_env_logger; extern crate pretty_env_logger;
#[macro_use] extern crate log; #[macro_use]
extern crate log;
mod args; 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() { fn main() {
// Initialize the logger // Initialize the logger
@ -18,9 +27,20 @@ fn main() {
info!("Successfully parsed arguments"); info!("Successfully parsed arguments");
trace!("Arguments: {:?}", 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 // Open the image
info!("Opening image: {}", arguments.image); info!("Opening image: {}", arguments.image);
let image = match image::open(arguments.image) { let image = match image::open(arguments.image.clone()) {
Ok(image) => image, Ok(image) => image,
Err(e) => { Err(e) => {
error!("Failed to open image: {:?}", e); error!("Failed to open image: {:?}", e);
@ -31,4 +51,39 @@ fn main() {
info!("Successfully opened image"); info!("Successfully opened image");
trace!("Image dimensions: {:?}", image.dimensions()); 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::<String>()) {
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");
}
}
} }