#![allow(clippy::missing_errors_doc)]
use std::process;
use std::fs::File;
use std::io::prelude::*;
use std::time::Duration;
use std::str;
use clap::{Parser, Subcommand};
use image::{imageops::FilterType, io::Reader as ImageReader, DynamicImage, GenericImageView, Pixel};
use miniz_oxide::deflate::compress_to_vec;
use miniz_oxide::inflate::decompress_to_vec;
use indicatif::{ProgressBar, ProgressStyle};
pub const APP_NAME: &str = env!("CARGO_PKG_NAME");
const GRAYSCALE: &str = "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
const SPINNER_TICK: u64 = 80;
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
command: Commands
}
impl Cli {
#![allow(clippy::must_use_candidate)]
pub fn get_command(&self) -> &Commands {
&self.command
}
}
#[derive(Subcommand)]
pub enum Commands {
Render {
input_file_path: String,
#[arg(short, long, value_name = "OUTPUT_FILE_PATH")]
output: String,
#[arg(short, long, value_name = "FLOAT", value_parser, num_args = 2, value_delimiter = ' ')]
#[arg(default_values_t = [1.0, 1.0])]
scale: Vec<f32>,
#[arg(short, long, value_name = "FLOAT")]
#[arg(value_parser = clap::value_parser!(f32))]
#[arg(default_value_t = 0.0)]
#[arg(allow_hyphen_values = true)]
contrast: f32
},
Open {
input_file_path: String
}
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_precision_loss)]
pub fn render(input_file_path: &String, output_file_path: &String, scale: &[f32], contrast: &f32) -> Result<(), String> {
if scale[0] < 0.0 || scale[1] < 0.0 {
return Err(String::from("Scale cannot be negative"));
}
let img = ImageReader::open(input_file_path).map_err(|e| format!("{input_file_path}: {e}"))?;
let spinner = ProgressBar::new_spinner();
spinner.set_style(ProgressStyle::with_template("{spinner:.default} {msg}").map_err(|e| format!("{e}"))?.tick_strings(&[
"[ ]",
"[= ]",
"[== ]",
"[=== ]",
"[====]",
"[ ===]",
"[ ==]",
"[ =]",
"[ ]",
"[ =]",
"[ ==]",
"[ ===]",
"[====]",
"[=== ]",
"[== ]",
"[= ]",
"[====]"
])
);
spinner.set_message("Decoding");
spinner.enable_steady_tick(Duration::from_millis(SPINNER_TICK));
let mut img_decoded = img.decode().map_err(|e| format!("{input_file_path}: {e}"))?;
spinner.set_message("Processing");
img_decoded = img_decoded
.resize_exact(
(img_decoded.width() as f32 * scale[0]) as u32,
(img_decoded.height() as f32 * scale[1]) as u32,
FilterType::Nearest
)
.grayscale()
.filter3x3(&[0.0, -1.0, 0.0, -1.0, 5.0, -1.0, 0.0, -1.0, 0.0])
.adjust_contrast(contrast.to_owned());
spinner.set_message("Conversion");
let mut ascii_img = convert_to_ascii(&img_decoded);
ascii_img.append(&mut format!("Scale: {}, {}\nContrast: {contrast}", scale[0], scale[1]).as_bytes().to_vec());
spinner.set_message("Compression");
ascii_img = compress_to_vec(&ascii_img, 10);
spinner.finish_with_message("Done");
let mut output_file = File::create(output_file_path).map_err(|e| format!("{output_file_path}: {e}"))?;
output_file.write_all(&ascii_img).map_err(|e| format!("{output_file_path}: {e}"))?;
Ok(())
}
fn convert_to_ascii(image: &DynamicImage) -> Vec<u8> {
let mut ascii_image = Vec::new();
for y in 0..image.height() {
for x in 0..image.width() {
ascii_image.push(GRAYSCALE
.as_bytes()
[
usize::from(image.get_pixel(x, y).channels()[0]) / 4
]
);
}
ascii_image.push(b'\n');
}
ascii_image
}
pub fn open(input_file_path: &String) -> Result<(), String> {
let mut input_file = File::open(input_file_path).map_err(|e| format!("{input_file_path}: {e}"))?;
let mut contents = Vec::new();
input_file.read_to_end(&mut contents).map_err(|e| format!("{input_file_path}: {e}"))?;
contents = decompress_to_vec(&contents).map_err(|e| format!("{e}"))?;
let contents_str = str::from_utf8(&contents).map_err(|e| format!("{e}"))?;
println!("{contents_str}");
Ok(())
}
pub fn handle_error(error: &str, code: i32) {
eprintln!("{APP_NAME}: {error}");
process::exit(code);
}