pub mod config;
mod pixel;
mod filter;
mod target;
use image::{DynamicImage, GenericImageView};
use once_cell::sync::Lazy;
pub use crate::config::ConfigBuilder;
use crate::config::{Config, ResizingDimension, TargetType};
pub fn convert(image: DynamicImage, config: &Config) -> String {
log::debug!("Using inverted color: {}", config.invert);
let input_width = image.width();
let input_height = image.height();
log::debug!("Input Image Width: {input_width}");
log::debug!("Input Image Height: {input_height}");
let (columns, rows, tile_width, tile_height) = ResizingDimension::calculate_dimensions(
config.target_size,
input_height,
input_width,
config.scale,
config.border,
config.dimension,
);
log::debug!("Columns: {columns}");
log::debug!("Rows: {rows}");
log::debug!("Tile Width: {tile_width}");
log::debug!("Tile Height: {tile_height}");
let mut input_img = image;
if config.outline {
input_img = filter::edge_detection_filter(input_img, config.hysteresis);
}
if config.transform_x {
log::info!("Flipping image horizontally");
input_img = input_img.fliph();
}
if config.transform_y {
log::info!("Flipping image vertically");
input_img = input_img.flipv();
}
log::info!("Resizing image to fit new dimensions");
let source_img = input_img.thumbnail_exact(columns * tile_width, rows * tile_height);
log::debug!("Resized Image Width: {}", source_img.width());
log::debug!("Resized Image Height: {}", source_img.height());
let mut output = String::with_capacity((tile_width * tile_height) as usize);
log::trace!("Created output string");
if config.target == TargetType::HtmlFile {
log::trace!("Adding html top part");
output.push_str(&target::html::html_top());
}
log::trace!("Calculating horizontal spacing");
let horizontal_spacing = if config.center_x {
spacing_horizontal(if config.border {
columns + 2
} else {
columns
})
} else {
String::with_capacity(0)
};
if config.center_y && config.target == TargetType::Shell {
log::trace!("Adding vertical top spacing");
output.push_str(&spacing_vertical(if config.border {
rows + 2
} else {
rows
}));
}
if config.border {
if config.center_x {
output.push_str(&horizontal_spacing);
}
log::trace!("Adding top part of border");
output.push('╔');
output.push_str(&"═".repeat(columns as usize));
output.push_str("╗\n");
}
log::info!("Starting conversion to ascii");
let width = source_img.width();
let target = source_img
.pixels()
.step_by(tile_width as usize)
.filter(|(x, y, _)| y % tile_height == 0 && x % tile_width == 0)
.map(|(x, y, _)| {
let mut pixels = Vec::with_capacity((tile_height * tile_width) as usize);
for p_x in 0..tile_width {
for p_y in 0..tile_height {
pixels.push(unsafe { source_img.unsafe_get_pixel(x + p_x, y + p_y) })
}
}
let mut ascii_char = pixel::correlating_char(&pixels, config);
if x == 0 {
if config.border {
ascii_char.insert(0, '║');
}
if config.center_x {
ascii_char.insert_str(0, &horizontal_spacing);
}
}
if x == width - tile_width {
if config.border {
ascii_char.push('║');
}
ascii_char.push('\n');
}
ascii_char
})
.collect::<String>();
output.push_str(&target);
if config.border {
if config.center_x {
output.push_str(&horizontal_spacing);
}
log::trace!("Adding bottom border");
output.push('╚');
output.push_str(&"═".repeat(columns as usize));
output.push('╝');
}
if config.target == TargetType::HtmlFile {
log::trace!("Adding html bottom part");
output.push_str(&target::html::html_bottom());
}
if config.center_y && config.target == TargetType::Shell {
log::trace!("Adding vertical bottom spacing");
output.push_str(&spacing_vertical(if config.border {
rows + 2
} else {
rows
}));
}
output
}
fn spacing_horizontal(width: u32) -> String {
let term_width = terminal_size::terminal_size()
.map(|dimensions| dimensions.0 .0 as u32)
.unwrap_or_default();
" ".repeat(term_width.saturating_sub(width).saturating_div(2) as usize)
}
fn spacing_vertical(height: u32) -> String {
let term_height = terminal_size::terminal_size()
.map(|dimensions| dimensions.1 .0 as u32)
.unwrap_or_default();
log::trace!("H: {term_height}, h: {height}");
"\n".repeat(term_height.saturating_sub(height).saturating_div(2) as usize)
}
pub static SUPPORTS_TRUECOLOR: Lazy<bool> = Lazy::new(|| {
std::env::var("COLORTERM")
.is_ok_and(|value| value.contains("truecolor") || value.contains("24bit"))
});