use clap::Parser;
use clap::builder as clap_builder;
use clap::builder::styling as clap_styling;
use imgii::error::FontError;
use imgii::error::ImgiiError;
use imgii::fonts::list_fonts;
use imgii::fonts::load_monospace_font;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use std::{sync::Arc, time::Instant};
use imgii::{
convert_to_ascii_gif, convert_to_ascii_png,
image_types::{IMG_TYPES_ARRAY, ImageBatchType, OutputImageType},
options::{
Charset, ImgiiOptions, ImgiiOptionsBuilder, convert_string_to_str_vec, from_enum,
to_charset_enum,
},
};
#[derive(Debug, Parser)]
#[command(author, version, about, styles=set_color_style())]
struct Args {
input_filename: String,
output_filename: String,
#[arg(short, long)]
width: Option<u32>,
#[arg(short = 'H', long)]
height: Option<u32>,
#[arg(short = 'n', long)]
font_name: Option<String>,
#[arg(short, long)]
font_size: Option<u32>,
#[arg(short, long)]
invert: bool,
#[arg(short, long)]
background: bool,
final_image_index: Option<u32>,
#[arg(short = 'C', long, default_value = "minimal")]
charset: String,
#[arg(short = 'o', long)]
char_override: Option<String>,
}
const DEFAULT_WIDTH: u32 = 128;
fn set_color_style() -> clap_builder::Styles {
clap_builder::Styles::styled()
.usage(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::Yellow,
))),
)
.header(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::BrightMagenta,
))),
)
.literal(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::Magenta,
))),
)
.error(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::BrightRed,
))),
)
.context_value(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::BrightCyan,
))),
)
.context(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::Green,
))),
)
.invalid(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::Red,
))),
)
.placeholder(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::Blue,
))),
)
.valid(
clap_styling::Style::new().fg_color(Some(clap_styling::Color::Ansi(
clap_styling::AnsiColor::BrightGreen,
))),
)
}
#[inline(always)]
fn setup_threads() {
let the_num_cpus = num_cpus::get();
let err = rayon::ThreadPoolBuilder::new()
.num_threads(the_num_cpus)
.build_global();
if let Err(err) = err {
panic!(
"Could not create a thread pool for program. Has it been created already? Num threads = {the_num_cpus}. ({err})"
);
}
}
fn imgii_builder_load_font<'a>(
font_name: Option<String>,
builder: ImgiiOptionsBuilder<'a>,
) -> Result<ImgiiOptionsBuilder<'a>, ImgiiError> {
let font_name = {
match font_name {
Some(font_name) => font_name,
None => {
let mut fonts = list_fonts();
log::debug!("Found fonts installed on system: {:?}", fonts);
assert!(
!fonts.is_empty(),
"there are no monospace truetype (.ttf) fonts installed that could be found"
);
fonts.swap_remove(0)
}
}
};
log::debug!("Attempting to load font {}", font_name);
match load_monospace_font(&font_name) {
Some((font, _)) => {
Ok(builder.font(font).font_name(font_name))
}
None => {
Err(ImgiiError::Font(FontError::FontLoad { font_name }))
}
}
}
fn create_imgii_options<'a>(
args: Args,
rascii_charset: Charset,
) -> Result<ImgiiOptions<'a>, ImgiiError> {
let mut builder: ImgiiOptionsBuilder<'a> =
ImgiiOptionsBuilder::new().background(args.background);
builder = imgii_builder_load_font(args.font_name, builder)?;
if let Some(font_size) = args.font_size {
builder = builder.font_size(font_size);
}
if let Some(width) = args.width {
builder = builder.width(width);
}
if let Some(height) = args.height {
builder = builder.height(height);
}
if let Some(char_override) = args.char_override {
builder = builder.char_override(convert_string_to_str_vec(&char_override));
}
builder
.invert(args.invert)
.charset(from_enum(rascii_charset))
.build()
}
fn main() {
let mut args = Args::parse();
env_logger::init();
setup_threads();
if args.width.is_none() && args.height.is_none() {
args.width = Some(DEFAULT_WIDTH);
}
let input_name_format = args.input_filename.clone();
let output_name_format = args.output_filename.clone();
let image_type = match OutputImageType::from_file_name(&args.output_filename) {
Some(image_type) => image_type,
None => {
panic!(
"Could not get output file type from {}, expected one of ({})",
args.output_filename,
IMG_TYPES_ARRAY.join(", ")
);
}
};
let rascii_charset = to_charset_enum(&args.charset).unwrap_or(Charset::Minimal);
let batch_type = if let Some(final_image_idx) = args.final_image_index {
ImageBatchType::Batch {
final_index: final_image_idx,
}
} else {
ImageBatchType::Single
};
let Ok(imgii_options) = create_imgii_options(args, rascii_charset) else {
panic!("could not create imgii options");
};
log::debug!("imgii options = {}", imgii_options);
match image_type {
OutputImageType::Png => {
match batch_type {
ImageBatchType::Batch {
final_index: final_image_idx,
} => {
log::debug!("Converting batch of PNGs...");
convert_png_batch(
final_image_idx,
Arc::from(input_name_format),
Arc::from(output_name_format),
Arc::from(imgii_options),
);
}
ImageBatchType::Single => {
log::debug!("Converting single PNG...");
match convert_to_ascii_png(
&input_name_format,
&output_name_format,
&imgii_options,
) {
Ok(_) => {}
Err(_) => {
log::error!("Could not save PNG {}", output_name_format);
}
};
}
};
}
OutputImageType::Gif => {
match batch_type {
ImageBatchType::Batch {
final_index: final_img_idx,
} => {
panic!(
"Cannot convert a batch of GIFs, argument final_img_idx={final_img_idx}. {}",
"Do not set this argument if intending to convert a GIF."
);
}
ImageBatchType::Single => {
log::debug!("Converting single GIF");
match convert_to_ascii_gif(
&input_name_format,
&output_name_format,
&imgii_options,
) {
Ok(_) => {
log::info!("Saved GIF {}", output_name_format);
}
Err(err) => {
log::error!("Could not save GIF {} ({})", output_name_format, err);
}
}
}
};
}
}
}
fn convert_png_batch(
final_image_index: u32,
input_name_format: Arc<String>,
output_name_format: Arc<String>,
imgii_options: Arc<ImgiiOptions>,
) {
let starting_time = Instant::now();
(1..=final_image_index).into_par_iter().for_each(|i| {
let input_name_format_arc = Arc::clone(&input_name_format);
let output_name_format_arc = Arc::clone(&output_name_format);
let imgii_options_arc = Arc::clone(&imgii_options);
let input_file_name = input_name_format_arc.replace("%d", i.to_string().as_str());
let output_file_name = output_name_format_arc.replace("%d", i.to_string().as_str());
match convert_to_ascii_png(&input_file_name, &output_file_name, &imgii_options_arc) {
Ok(_) => {
log::info!("Saved PNG {}", output_file_name);
}
Err(err) => {
panic!("Could not save PNG {} ({})", output_file_name, err);
}
};
});
log::info!("---Success!---");
log::info!(
"Time elapsed: {} seconds / {} milliseconds",
starting_time.elapsed().as_secs(),
starting_time.elapsed().as_millis()
);
}