use image::{DynamicImage, GenericImageView, GrayImage, io::Reader as ImageReader, imageops::FilterType};
use std::path::Path;
use std::fs;
const ASCII_CHARS: &[u8] = b"@%#*+=-:. ";
const DETAILED_ASCII_CHARS: &[u8] = b"$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ";
const HIGH_DENSITY_CHARS: &[&str] = &[
"█", "▓", "▒", "░", "▄", "▀", "■", "▪", "●", "◆",
"◉", "◍", "◎", "○", "☉", "◌", "◊", "♦", "♢", "•",
".", " "
];
#[derive(Debug, Clone)]
pub struct AsciiConfig {
pub width: u32,
pub height: u32,
pub use_detailed_chars: bool,
pub use_high_density: bool,
pub use_color: bool,
pub color_saturation: f32,
pub invert: bool,
pub contrast: f32,
pub brightness: f32,
pub scale: f32,
}
impl Default for AsciiConfig {
fn default() -> Self {
Self {
width: 100,
height: 50,
use_detailed_chars: false,
use_high_density: false,
use_color: false,
color_saturation: 0.7,
invert: false,
contrast: 1.0,
brightness: 1.0,
scale: 1.0,
}
}
}
pub struct AsciiConverter {
config: AsciiConfig,
}
impl AsciiConverter {
pub fn new(width: u32, height: u32) -> Self {
Self {
config: AsciiConfig {
width,
height,
..Default::default()
}
}
}
pub fn with_config(config: AsciiConfig) -> Self {
Self { config }
}
pub fn convert(&self, image_path: &str) -> Result<String, String> {
let img = ImageReader::open(&Path::new(image_path))
.map_err(|e| format!("Gagal membuka gambar: {}", e))?
.decode()
.map_err(|e| format!("Gagal mendekode gambar: {}", e))?;
self.process_image(&img)
}
pub fn convert_from_bytes(&self, bytes: &[u8]) -> Result<String, String> {
let img = image::load_from_memory(bytes)
.map_err(|e| format!("Gagal memuat gambar dari bytes: {}", e))?;
self.process_image(&img)
}
fn process_image(&self, img: &DynamicImage) -> Result<String, String> {
let target_width = (self.config.width as f32 * self.config.scale) as u32;
let target_height = (self.config.height as f32 * self.config.scale) as u32;
let mut processed = img.resize_exact(
target_width,
target_height,
FilterType::Lanczos3
);
processed = self.apply_image_adjustments(&processed);
if self.config.use_color {
Ok(self.image_to_colored_ascii(&processed))
} else {
let grayscale = processed.into_luma8();
Ok(self.image_to_ascii(&grayscale))
}
}
fn image_to_ascii(&self, image: &GrayImage) -> String {
let mut ascii_output = String::new();
if self.config.use_high_density {
for y in 0..self.config.height {
for x in 0..self.config.width {
let scale_factor = self.config.scale as u32;
let base_x = (x * scale_factor) as u32;
let base_y = (y * scale_factor) as u32;
let mut total_brightness = 0.0;
let mut count = 0.0;
for dy in 0..scale_factor {
for dx in 0..scale_factor {
if base_x + dx < image.width() && base_y + dy < image.height() {
let pixel = image.get_pixel(base_x + dx, base_y + dy);
let mut brightness = pixel[0] as f32 / 255.0;
if self.config.invert {
brightness = 1.0 - brightness;
}
total_brightness += brightness;
count += 1.0;
}
}
}
let avg_brightness = if count > 0.0 { total_brightness / count } else { 0.0 };
let index = (avg_brightness * (HIGH_DENSITY_CHARS.len() - 1) as f32) as usize;
ascii_output.push_str(HIGH_DENSITY_CHARS[index]);
}
ascii_output.push('\n');
}
} else {
let chars = if self.config.use_detailed_chars {
DETAILED_ASCII_CHARS
} else {
ASCII_CHARS
};
for y in 0..self.config.height {
for x in 0..self.config.width {
let scale_factor = self.config.scale as u32;
let base_x = (x * scale_factor) as u32;
let base_y = (y * scale_factor) as u32;
let mut total_brightness = 0.0;
let mut count = 0.0;
for dy in 0..scale_factor {
for dx in 0..scale_factor {
if base_x + dx < image.width() && base_y + dy < image.height() {
let pixel = image.get_pixel(base_x + dx, base_y + dy);
let mut brightness = pixel[0] as f32 / 255.0;
if self.config.invert {
brightness = 1.0 - brightness;
}
total_brightness += brightness;
count += 1.0;
}
}
}
let avg_brightness = if count > 0.0 {
total_brightness / count
} else {
0.0
};
let index = (avg_brightness * (chars.len() - 1) as f32) as usize;
ascii_output.push(chars[index] as char);
}
ascii_output.push('\n');
}
}
ascii_output
}
fn image_to_colored_ascii(&self, image: &DynamicImage) -> String {
let mut html_output = String::from(
"<!DOCTYPE html>\n<html>\n<head>\n<style>\n\
body { background-color: #000; margin: 0; padding: 10px; }\n\
pre { font-family: monospace; font-size: 10px; line-height: 0.9; }\n\
</style>\n</head>\n<body>\n<pre>\n"
);
let chars = if self.config.use_detailed_chars {
DETAILED_ASCII_CHARS
} else if self.config.use_high_density {
b"@%#*+=-:. "
} else {
ASCII_CHARS
};
for y in 0..self.config.height {
for x in 0..self.config.width {
let scale_factor = self.config.scale as u32;
let base_x = (x * scale_factor) as u32;
let base_y = (y * scale_factor) as u32;
let mut total_r = 0.0;
let mut total_g = 0.0;
let mut total_b = 0.0;
let mut total_brightness = 0.0;
let mut count = 0.0;
for dy in 0..scale_factor {
for dx in 0..scale_factor {
if base_x + dx < image.width() && base_y + dy < image.height() {
let pixel = image.get_pixel(base_x + dx, base_y + dy);
let r = pixel[0] as f32 / 255.0;
let g = pixel[1] as f32 / 255.0;
let b = pixel[2] as f32 / 255.0;
let brightness = r * 0.3 + g * 0.59 + b * 0.11;
total_r += r;
total_g += g;
total_b += b;
total_brightness += brightness;
count += 1.0;
}
}
}
if count > 0.0 {
let avg_r = total_r / count;
let avg_g = total_g / count;
let avg_b = total_b / count;
let avg_brightness = if self.config.invert {
1.0 - (total_brightness / count)
} else {
total_brightness / count
};
let char_index = (avg_brightness * (chars.len() - 1) as f32) as usize;
let character = if self.config.use_high_density && char_index < HIGH_DENSITY_CHARS.len() {
HIGH_DENSITY_CHARS[char_index].to_string()
} else {
let char_bytes = &[chars[char_index]];
std::str::from_utf8(char_bytes)
.map(|s| s.to_string())
.unwrap_or_else(|_| " ".to_string())
};
let sat = self.config.color_saturation;
let r = ((avg_r * sat + (1.0 - sat) * 0.5) * 255.0) as u8;
let g = ((avg_g * sat + (1.0 - sat) * 0.5) * 255.0) as u8;
let b = ((avg_b * sat + (1.0 - sat) * 0.5) * 255.0) as u8;
html_output.push_str(&format!("<span style=\"color:rgb({},{},{})\">{}</span>", r, g, b, character));
}
}
html_output.push_str("<br/>\n");
}
html_output.push_str("</pre>\n</body>\n</html>");
html_output
}
pub fn save_to_file(&self, ascii: &str, output_path: &str) -> Result<(), String> {
fs::write(output_path, ascii)
.map_err(|e| format!("Gagal menyimpan file: {}", e))
}
fn apply_image_adjustments(&self, img: &DynamicImage) -> DynamicImage {
let mut adjusted = img.to_rgba8();
for pixel in adjusted.pixels_mut() {
for c in 0..3 {
let mut color = pixel[c] as f32 / 255.0;
color = ((color - 0.5) * self.config.contrast + 0.5)
.clamp(0.0, 1.0);
color = (color * self.config.brightness)
.clamp(0.0, 1.0);
pixel[c] = (color * 255.0) as u8;
}
}
DynamicImage::ImageRgba8(adjusted)
}
}