sync with local; working normal/complex conversions

This commit is contained in:
Djairo Hougee 2023-02-08 15:48:20 +01:00
parent 82b85e15a0
commit 5a1eda07da
8 changed files with 271 additions and 0 deletions

6
.gitignore vendored
View File

@ -8,3 +8,9 @@ Cargo.lock
# These are backup files generated by rustfmt # These are backup files generated by rustfmt
**/*.rs.bk **/*.rs.bk
#intellij folder
.idea/
#test files
*.png

14
Cargo.toml Normal file
View File

@ -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.*"

View File

@ -1,2 +1,6 @@
# img2ascii # img2ascii
CLI that converts images into ascii art and prints them to the console CLI that converts images into ascii art and prints them to the console
//todo: Update this

44
src/ascii_manipulation.rs Normal file
View File

@ -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>)) -> 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<String> {
//todo: add color support
let l = char_map.len() as f32;
let mut str = String::new();
let mut out: Vec<String> = 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<String> {
to_ascii(" .:-=+*#%@".to_owned(), image)
}
pub fn to_complex_ascii(image: DynamicImage) -> Vec<String> {
to_ascii(" .'`^\",:;Il!i><~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$".to_owned(), image)
}
pub fn to_braille_ascii(image: DynamicImage) -> Vec<String> {
//todo: figure out braille symbols
vec!["not implemented".to_owned()]
}
pub fn to_custom_ascii(char_map: String, image: DynamicImage) -> Vec<String> {
//todo: this
vec!["not implemented".to_owned()]
}

113
src/cli.rs Normal file
View File

@ -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<u16>,
/// Save the output to a file, instead of printing to terminal
#[arg(short = 'o', long = "output")]
pub output: Option<PathBuf>,
}
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
}
}
}

38
src/image_manipulation.rs Normal file
View File

@ -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<u16>, 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)
}
}
}

41
src/main.rs Normal file
View File

@ -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
*/
}

11
src/output.rs Normal file
View File

@ -0,0 +1,11 @@
use std::path::PathBuf;
pub fn print_terminal(ascii: Vec<String>) {
for line in ascii {
println!("{}", line);
}
}
pub fn print_file(ascii: Vec<String>, out: PathBuf) {
//todo: output
}