From 34f2a1ef13b1e3fefb3c970043d9807784c67b6b Mon Sep 17 00:00:00 2001 From: Waveplate Date: Sun, 15 Dec 2024 07:33:48 -0800 Subject: [PATCH] improve quarterblock rendering and add braille output mode with luma-only color manipulation --- src/draw.rs | 899 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 679 insertions(+), 220 deletions(-) diff --git a/src/draw.rs b/src/draw.rs index cf99f2c..ae70664 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,45 +1,45 @@ use crate::args; use crate::palette::{RGB99, RGB88, ANSI232, ANSI256, nearest_hex_color}; use photon_rs::PhotonImage; +use std::collections::HashMap; -// █ full -const FULL: &str = "\u{2588}"; - -// ▄ down const UP: &str = "\u{2580}"; -// ▀ up -const DOWN: &str = "\u{2584}"; +static QUARTER_BLOCKS: [&str; 16] = [ + " ", // 0b0000 + "\u{2597}", // ▗ Quadrant lower right + "\u{2596}", // ▖ Quadrant lower left + "\u{2584}", // ▄ Lower half block + "\u{259D}", // ▝ Quadrant upper right + "\u{2590}", // ▐ Right half block + "\u{259E}", // ▞ Quadrant upper right and lower left + "\u{259F}", // ▟ Quadrant upper right, lower left, lower right + "\u{2598}", // ▘ Quadrant upper left + "\u{259A}", // ▚ Quadrant upper left and lower right + "\u{258C}", // ▌ Left half block + "\u{2599}", // ▙ Quadrant upper left, lower left, lower right + "\u{2580}", // ▀ Upper half block + "\u{259C}", // ▜ Quadrant upper left, upper right, lower right + "\u{259B}", // ▛ Quadrant upper left, upper right, lower left + "\u{2588}", // █ Full block +]; -// ▌ left -const LEFT: &str = "\u{258C}"; - -// ▐ right -const RIGHT: &str = "\u{2590}"; - -// ▞ diag_right -const DIAG_RIGHT: &str = "\u{259E}"; - -// ▚ diag_left -const DIAG_LEFT: &str = "\u{259A}"; - -// ▙ down_left (2596 prev) -const DOWN_LEFT: &str = "\u{2599}"; - -// ▟ down_right -const DOWN_RIGHT: &str = "\u{259F}"; - -// ▛ top_left -const UP_LEFT: &str = "\u{259B}"; - -// ▜ top_right -const UP_RIGHT: &str = "\u{259C}"; +const POSITIONS: [(usize, usize, u32); 8] = [ + (0, 0, 0x01), // dot 1 + (0, 1, 0x02), // dot 2 + (0, 2, 0x04), // dot 3 + (1, 0, 0x08), // dot 4 + (1, 1, 0x10), // dot 5 + (1, 2, 0x20), // dot 6 + (0, 3, 0x40), // dot 7 + (1, 3, 0x80), // dot 8 +]; #[derive(Debug, Clone)] pub struct AnsiImage { - pub image: PhotonImage, pub bitmap: Vec>, pub halfblock: Vec>, + pub quarterblock: Vec>, } #[derive(Debug, Clone, Copy)] @@ -57,6 +57,14 @@ pub struct AnsiPixel { pub irc88: u8, } +#[derive(Debug, Clone, Copy)] +pub struct AnsiPixelQuad { + pub top_left: AnsiPixel, + pub top_right: AnsiPixel, + pub bottom_left: AnsiPixel, + pub bottom_right: AnsiPixel, +} + impl AnsiPixel { pub fn new(pixel: &u32) -> AnsiPixel { let irc = nearest_hex_color(*pixel, RGB99.to_vec()); @@ -65,17 +73,18 @@ impl AnsiPixel { let ansi232 = nearest_hex_color(*pixel, ANSI232.to_vec()); AnsiPixel { orig: *pixel, - ansi: ansi, - ansi232: ansi232, - irc: irc, - irc88: irc88, + ansi, + ansi232, + irc, + irc88, } } } impl AnsiImage { pub fn new(image: PhotonImage) -> AnsiImage { - let mut bitmap = image.get_raw_pixels() + let mut bitmap = image + .get_raw_pixels() .chunks(4) .map(|x| make_rgb_u32(x.to_vec())) .collect::>() @@ -87,12 +96,19 @@ impl AnsiImage { bitmap.push(vec![0; image.get_width() as usize]); } + for row in &mut bitmap { + if row.len() % 2 != 0 { + row.push(0); + } + } + let halfblock = halfblock_bitmap(&bitmap); - - return AnsiImage { - image: image, - bitmap: bitmap, - halfblock: halfblock, + let quarterblock = quarterblock_bitmap(&bitmap); + + AnsiImage { + bitmap, + halfblock, + quarterblock, } } } @@ -102,39 +118,72 @@ pub fn make_rgb_u8(rgb: u32) -> [u8; 3] { let g = (rgb >> 8) as u8; let b = rgb as u8; - return [r, g, b] + [r, g, b] } pub fn make_rgb_u32(rgb: Vec) -> u32 { - let r = *rgb.get(0).unwrap() as u32; - let g = *rgb.get(1).unwrap() as u32; - let b = *rgb.get(2).unwrap() as u32; + let r = rgb[0] as u32; + let g = rgb[1] as u32; + let b = rgb[2] as u32; - let rgb = (r << 16) + (g << 8) + b; + (r << 16) + (g << 8) + b +} - return rgb +pub fn quarterblock_bitmap(bitmap: &Vec>) -> Vec> { + let ansi_bitmap = bitmap + .iter() + .map(|x| x.iter().map(|y| AnsiPixel::new(y)).collect::>()) + .collect::>>(); + + let mut ansi_canvas: Vec> = Vec::new(); + + for two_rows in ansi_bitmap.chunks(2) { + let top_row = &two_rows[0]; + let bottom_row = &two_rows[1]; + + let mut ansi_row: Vec = Vec::new(); + + for i in (0..top_row.len()).step_by(2) { + let default_pixel = AnsiPixel::new(&0); + let top_left_pixel = top_row.get(i).unwrap_or(&default_pixel); + let top_right_pixel = top_row.get(i + 1).unwrap_or(&default_pixel); + let bottom_left_pixel = bottom_row.get(i).unwrap_or(&default_pixel); + let bottom_right_pixel = bottom_row.get(i + 1).unwrap_or(&default_pixel); + + let pixel_quad = AnsiPixelQuad { + top_left: *top_left_pixel, + top_right: *top_right_pixel, + bottom_left: *bottom_left_pixel, + bottom_right: *bottom_right_pixel, + }; + + ansi_row.push(pixel_quad); + } + + ansi_canvas.push(ansi_row); + } + + ansi_canvas } pub fn halfblock_bitmap(bitmap: &Vec>) -> Vec> { let ansi_bitmap = bitmap - .iter() - .map(|x| { - x.iter().map(|y| AnsiPixel::new(y)).collect::>() - }) - .collect::>>(); + .iter() + .map(|x| x.iter().map(|y| AnsiPixel::new(y)).collect::>()) + .collect::>>(); let mut ansi_canvas: Vec> = Vec::new(); for two_rows in ansi_bitmap.chunks(2) { - let rows = two_rows.to_vec(); - let top_row = rows.get(0).unwrap(); - let bottom_row = rows.get(1).unwrap(); + let top_row = &two_rows[0]; + let bottom_row = &two_rows[1]; let mut ansi_row: Vec = Vec::new(); - for i in 0..bitmap.get(0).unwrap().len() { - let top_pixel = top_row.get(i as usize).unwrap(); - let bottom_pixel = bottom_row.get(i as usize).unwrap(); + for i in 0..top_row.len() { + let default_pixel = AnsiPixel::new(&0); + let top_pixel = top_row.get(i).unwrap_or(&default_pixel); + let bottom_pixel = bottom_row.get(i).unwrap_or(&default_pixel); let pixel_pair = AnsiPixelPair { top: *top_pixel, @@ -150,231 +199,641 @@ pub fn halfblock_bitmap(bitmap: &Vec>) -> Vec> { ansi_canvas } -fn get_qb_char(pixel_pairs: &[AnsiPixelPair]) -> &str { - let (pair0_top, pair0_bottom) = (&pixel_pairs[0].top.irc, &pixel_pairs[0].bottom.irc); - let (pair1_top, pair1_bottom) = (&pixel_pairs[1].top.irc, &pixel_pairs[1].bottom.irc); +fn get_qb_char(pixel_quad: &AnsiPixelQuad, fg_color: u8, args: &args::Args) -> &'static str { - let ups_equal = pair0_top == pair1_top; - let downs_equal = pair0_bottom == pair1_bottom; - let lefts_equal = pair0_top == pair0_bottom; - let rights_equal = pair1_top == pair1_bottom; - let left_diag = pair0_top == pair1_bottom; - let right_diag = pair1_top == pair0_bottom; + let c0 = if args.nograyscale { + pixel_quad.top_left.irc88 + } else { + pixel_quad.top_left.irc + }; // top-left + let c1 = if args.nograyscale { + pixel_quad.top_right.irc88 + } else { + pixel_quad.top_right.irc + }; // top-right + let c2 = if args.nograyscale { + pixel_quad.bottom_left.irc88 + } else { + pixel_quad.bottom_left.irc + }; // bottom-left + let c3 = if args.nograyscale { + pixel_quad.bottom_right.irc88 + } else { + pixel_quad.bottom_right.irc + }; // bottom-right - match (ups_equal, downs_equal, lefts_equal, rights_equal, left_diag, right_diag) { - (true, _, true, true, _, _) => FULL, - (true, _, true, _, _, _) => UP_LEFT, - (true, _, _, true, _, _) => UP_RIGHT, - (true, _, _, _, _, _) => UP, - (_, true, true, _, _, _) => DOWN_LEFT, - (_, true, _, true, _, _) => DOWN_RIGHT, - (_, true, _, _, _, _) => DOWN, - (_, _, true, false, _, _) => LEFT, - (_, _, false, true, _, _) => RIGHT, - (_, _, _, _, true, _) => DIAG_LEFT, - (_, _, _, _, _, true) => DIAG_RIGHT, - _ => UP, + let mut pattern = 0; + + if c2 == fg_color { + pattern |= 1 << 0; // bit 0 (bottom-left) } + if c3 == fg_color { + pattern |= 1 << 1; // bit 1 (bottom-right) + } + if c0 == fg_color { + pattern |= 1 << 2; // bit 2 (top-left) + } + if c1 == fg_color { + pattern |= 1 << 3; // bit 3 (top-right) + } + + QUARTER_BLOCKS[pattern as usize] } -pub fn ansi_draw_24bit(image: AnsiImage) -> String { +pub fn irc_draw_qb(image: &AnsiImage, args: &args::Args) -> String { let mut out: String = String::new(); - for (y, row) in image.halfblock.iter().enumerate() { + for (y, row) in image.quarterblock.iter().enumerate() { + let mut last_fg: u8 = 255; + let mut last_bg: u8 = 255; + for (x, pixel_quad) in row.iter().enumerate() { + + let c0 = if args.nograyscale { + pixel_quad.top_left.irc88 + } else { + pixel_quad.top_left.irc + }; + let c1 = if args.nograyscale { + pixel_quad.top_right.irc88 + } else { + pixel_quad.top_right.irc + }; + + let c2 = if args.nograyscale { + pixel_quad.bottom_left.irc88 + } else { + pixel_quad.bottom_left.irc + }; + + let c3 = if args.nograyscale { + pixel_quad.bottom_right.irc88 + } else { + pixel_quad.bottom_right.irc + }; + + let mut color_counts = HashMap::new(); + for &color in &[c0, c1, c2, c3] { + *color_counts.entry(color).or_insert(0) += 1; + } + + let bg_color = *color_counts + .iter() + .max_by_key(|entry| entry.1) + .map(|(color, _)| color) + .unwrap_or(&0); + let fg_color = *color_counts + .iter() + .filter(|&(color, _)| color != &bg_color) + .map(|(color, _)| color) + .next() + .unwrap_or(&bg_color); + + let char = get_qb_char(pixel_quad, fg_color, args); + + if x == 0 || fg_color != last_fg || bg_color != last_bg { + if fg_color == bg_color { + out.push_str(&format!("\x03{}{}", fg_color, char)); + } else { + out.push_str(&format!("\x03{},{}{}", fg_color, bg_color, char)); + } + } else { + out.push_str(&char); + } + + last_fg = fg_color; + last_bg = bg_color; + } + + out.push_str("\x0f"); + + if y != image.quarterblock.len() - 1 { + out.push_str("\n"); + } + } + out.trim_end().to_string() +} + +pub fn ansi_draw_24bit_qb(image: &AnsiImage) -> String { + let mut out: String = String::new(); + for row in &image.quarterblock { + for pixel_quad in row.iter() { + + let c0_rgb = make_rgb_u8(pixel_quad.top_right.orig); + let c1_rgb = make_rgb_u8(pixel_quad.top_left.orig); + let c2_rgb = make_rgb_u8(pixel_quad.bottom_right.orig); + let c3_rgb = make_rgb_u8(pixel_quad.bottom_left.orig); + + let mut color_counts = HashMap::new(); + for &rgb in &[c0_rgb, c1_rgb, c2_rgb, c3_rgb] { + *color_counts.entry(rgb).or_insert(0) += 1; + } + + let bg_color = *color_counts + .iter() + .max_by_key(|entry| entry.1) + .map(|(rgb, _)| rgb) + .unwrap_or(&[0, 0, 0]); + let fg_color = *color_counts + .iter() + .filter(|&(rgb, _)| rgb != &bg_color) + .map(|(rgb, _)| rgb) + .next() + .unwrap_or(&bg_color); + + let char = { + let mut pattern = 0; + if c2_rgb == fg_color { + pattern |= 1 << 0; // bit 0 (bottom-left) + } + if c3_rgb == fg_color { + pattern |= 1 << 1; // bit 1 (bottom-right) + } + if c0_rgb == fg_color { + pattern |= 1 << 2; // bit 2 (top-left) + } + if c1_rgb == fg_color { + pattern |= 1 << 3; // bit 3 (top-right) + } + QUARTER_BLOCKS[pattern as usize] + }; + + out.push_str(&format!( + "\x1b[38;2;{};{};{}m\x1b[48;2;{};{};{}m{}", + fg_color[0], + fg_color[1], + fg_color[2], + bg_color[0], + bg_color[1], + bg_color[2], + char + )); + } + out.push_str("\x1b[0m\n"); + } + out.trim_end().to_string() +} + +pub fn ansi_draw_8bit_qb(image: &AnsiImage, args: &args::Args) -> String { + let mut out: String = String::new(); + for row in &image.quarterblock { + for pixel_quad in row.iter() { + + let c0 = if args.nograyscale { + pixel_quad.top_right.ansi232 + } else { + pixel_quad.top_right.ansi + }; + let c1 = if args.nograyscale { + pixel_quad.top_left.ansi232 + } else { + pixel_quad.top_left.ansi + }; + let c2 = if args.nograyscale { + pixel_quad.bottom_right.ansi232 + } else { + pixel_quad.bottom_right.ansi + }; + let c3 = if args.nograyscale { + pixel_quad.bottom_left.ansi232 + } else { + pixel_quad.bottom_left.ansi + }; + + let mut color_counts = HashMap::new(); + for &color in &[c0, c1, c2, c3] { + *color_counts.entry(color).or_insert(0) += 1; + } + + let bg_color = *color_counts + .iter() + .max_by_key(|entry| entry.1) + .map(|(color, _)| color) + .unwrap_or(&0); + let fg_color = *color_counts + .iter() + .filter(|&(color, _)| color != &bg_color) + .map(|(color, _)| color) + .next() + .unwrap_or(&bg_color); + + let char = { + let mut pattern = 0; + if c2 == fg_color { + pattern |= 1 << 0; // bit 0 (bottom-left) + } + if c3 == fg_color { + pattern |= 1 << 1; // bit 1 (bottom-right) + } + if c0 == fg_color { + pattern |= 1 << 2; // bit 2 (top-left) + } + if c1 == fg_color { + pattern |= 1 << 3; // bit 3 (top-right) + } + QUARTER_BLOCKS[pattern as usize] + }; + + out.push_str(&format!( + "\x1b[38;5;{}m\x1b[48;5;{}m{}", + fg_color, + bg_color, + char + )); + } + out.push_str("\x1b[0m\n"); + } + out.trim_end().to_string() +} + +pub fn ansi_draw_24bit(image: &AnsiImage) -> String { + let mut out: String = String::new(); + for row in &image.halfblock { for pixel_pair in row.iter() { let fg = make_rgb_u8(pixel_pair.top.orig) .to_vec() .iter() .map(|x| x.to_string()) .collect::>(); - + let bg = make_rgb_u8(pixel_pair.bottom.orig) .to_vec() .iter() .map(|x| x.to_string()) .collect::>(); - out.push_str(format!("\x1b[38;2;{}m\x1b[48;2;{}m{}", fg.join(";"), bg.join(";"), UP).as_str()); - } - out.push_str("\x1b[0m"); - - if y != image.halfblock.len() - 1 { - out.push_str("\n"); + out.push_str( + format!( + "\x1b[38;2;{}m\x1b[48;2;{}m{}", + fg.join(";"), + bg.join(";"), + UP + ) + .as_str(), + ); } + out.push_str("\x1b[0m\n"); } - return out + out.trim_end().to_string() } -pub fn ansi_draw_24bit_qb(image: AnsiImage) -> String { +pub fn ansi_draw_8bit(image: &AnsiImage, args: &args::Args) -> String { let mut out: String = String::new(); - for (y, row) in image.halfblock.iter().enumerate() { - for pixel_pairs in row.chunks(2) { - let fg = make_rgb_u8(pixel_pairs[0].top.orig) - .to_vec() - .iter() - .map(|x| x.to_string()) - .collect::>(); - - let bg = make_rgb_u8(pixel_pairs[0].bottom.orig) - .to_vec() - .iter() - .map(|x| x.to_string()) - .collect::>(); - - let char = match y { - _ if y == image.halfblock.len() - 1 => UP, - _ => get_qb_char(pixel_pairs), - }; - - out.push_str(format!("\x1b[38;2;{}m\x1b[48;2;{}m{}", fg.join(";"), bg.join(";"), char).as_str()); - } - out.push_str("\x1b[0m"); - - if y != image.halfblock.len() - 1 { - out.push_str("\n"); - } - } - return out -} - -pub fn ansi_draw_8bit(image: AnsiImage, args: &args::Args) -> String { - let mut out: String = String::new(); - for (y, row) in image.halfblock.iter().enumerate() { + for row in &image.halfblock { for pixel_pair in row.iter() { - - let fg = match args.nograyscale { - true => pixel_pair.top.ansi232, - false => pixel_pair.top.ansi, + let fg = if args.nograyscale { + pixel_pair.top.ansi232 + } else { + pixel_pair.top.ansi }; - let bg = match args.nograyscale { - true => pixel_pair.bottom.ansi232, - false => pixel_pair.bottom.ansi, + let bg = if args.nograyscale { + pixel_pair.bottom.ansi232 + } else { + pixel_pair.bottom.ansi }; out.push_str(format!("\x1b[38;5;{}m\x1b[48;5;{}m{}", fg, bg, UP).as_str()); } - out.push_str("\x1b[0m"); - - if y != image.halfblock.len() - 1 { - out.push_str("\n"); - } + out.push_str("\x1b[0m\n"); } - return out + out.trim_end().to_string() } -pub fn ansi_draw_8bit_qb(image: AnsiImage, args: &args::Args) -> String { +pub fn irc_draw(image: &AnsiImage, args: &args::Args) -> String { let mut out: String = String::new(); - for (y, row) in image.halfblock.iter().enumerate() { - for pixel_pairs in row.chunks(2) { - let fg = match args.nograyscale { - true => pixel_pairs[0].top.ansi232, - false => pixel_pairs[0].top.ansi, - }; - - let bg = match args.nograyscale { - true => pixel_pairs[0].bottom.ansi232, - false => pixel_pairs[0].bottom.ansi, - }; - - let char = match y { - _ if y == image.halfblock.len() - 1 => UP, - _ => get_qb_char(pixel_pairs), - }; - - out.push_str(format!("\x1b[38;5;{}m\x1b[48;5;{}m{}", fg, bg, char).as_str()); - } - out.push_str("\x1b[0m"); - - if y != image.halfblock.len() - 1 { - out.push_str("\n"); - } - } - return out -} - -pub fn irc_draw(image: AnsiImage, args: &args::Args) -> String { - let mut out: String = String::new(); - for (y, row) in image.halfblock.iter().enumerate() { - let mut last_fg: u8 = 0; - let mut last_bg: u8 = 0; + for row in &image.halfblock { + let mut last_fg: u8 = 255; + let mut last_bg: u8 = 255; for (x, pixel_pair) in row.iter().enumerate() { - let fg = match args.nograyscale { - true => pixel_pair.top.irc88, - false => pixel_pair.top.irc, + + let fg = if args.nograyscale { + pixel_pair.top.irc88 + } else { + pixel_pair.top.irc }; - let bg = match args.nograyscale { - true => pixel_pair.bottom.irc88, - false => pixel_pair.bottom.irc, + let bg = if args.nograyscale { + pixel_pair.bottom.irc88 + } else { + pixel_pair.bottom.irc }; - if x != 0 { - if fg == last_fg && bg == last_bg { - out.push_str(&format!("{}", UP)); - } else if bg == last_bg { + if x == 0 || fg != last_fg || bg != last_bg { + if last_bg == bg { out.push_str(&format!("\x03{}{}", fg, UP)); } else { out.push_str(&format!("\x03{},{}{}", fg, bg, UP)); } } else { - out.push_str(&format!("\x03{},{}{}", fg, bg, UP)); + out.push_str(&UP); } last_fg = fg; last_bg = bg; } - out.push_str("\x0f"); - - if y != image.halfblock.len() - 1 { - out.push_str("\n"); - } + out.push_str("\x0f\n"); } - return out + out.trim_end().to_string() } -pub fn irc_draw_qb(image: AnsiImage, args: &args::Args) -> String { +pub fn luma(rgb: &[u8; 3]) -> u8 { + let r = rgb[0] as u16; + let g = rgb[1] as u16; + let b = rgb[2] as u16; + ((0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32).round()) as u8 +} + +pub fn ansi_draw_braille_24bit(image_luma: &AnsiImage, image_chroma: &AnsiImage, args: &args::Args) -> String { let mut out: String = String::new(); - for (y, row) in image.halfblock.iter().enumerate() { - let mut last_fg: u8 = 0; - let mut last_bg: u8 = 0; - for (x, pixel_pairs) in row.chunks(2).enumerate() { - let fg = match args.nograyscale { - true => pixel_pairs[0].top.irc88, - false => pixel_pairs[0].top.irc, - }; + let bitmap_luma = &image_luma.bitmap; + let bitmap_chroma = &image_chroma.bitmap; + let height = bitmap_luma.len(); + let width = bitmap_luma[0].len(); - let bg = match args.nograyscale { - true => pixel_pairs[0].bottom.irc88, - false => pixel_pairs[0].bottom.irc, - }; + let mut error_matrix = vec![vec![0.0; width]; height]; + + let mut min_luma = 255u32; + let mut max_luma = 0u32; + for row in bitmap_luma.iter() { + for &pixel in row.iter() { + let l = luma(&make_rgb_u8(pixel)) as u32; + min_luma = min_luma.min(l); + max_luma = max_luma.max(l); + } + } - let char = match y { - _ if y == image.halfblock.len() - 1 => UP, - _ => get_qb_char(pixel_pairs), - }; + let luma_range = max_luma.saturating_sub(min_luma).max(1); - if x == 0 { - out.push_str(&format!("\x03{},{}{}", fg, bg, char)); - } else { - if fg == last_fg && bg == last_bg { - out.push_str(&format!("{}", char)); - } else if bg == last_bg { - out.push_str(&format!("\x03{}{}", fg, char)); - } else { - out.push_str(&format!("\x03{},{}{}", fg, bg, char)); + let scaled_threshold = min_luma + (luma_range / 2); + + for y in (0..height).step_by(4) { + let mut last_fg = [0u8; 3]; + let mut first = true; + for x in (0..width).step_by(2) { + let mut braille_char = 0x2800; + let mut color_counts: HashMap<[u8; 3], u32> = HashMap::new(); + + for &(dx, dy, bit) in &POSITIONS { + let current_y = y + dy; + let current_x = x + dx; + if current_y < height && current_x < width { + let original_pixel = bitmap_luma[current_y][current_x]; + let rgb = make_rgb_u8(original_pixel); + let current_luma = luma(&rgb) as f64; + + let mut pixel_luma = current_luma + error_matrix[current_y][current_x]; + pixel_luma = pixel_luma.clamp(0.0, 255.0); + + let new_pixel = if pixel_luma > scaled_threshold as f64 { + 255.0 + } else { + 0.0 + }; + + let error = pixel_luma - new_pixel; + + if new_pixel == 255.0 { + braille_char |= bit; + + let px_chroma = bitmap_chroma[current_y][current_x]; + let rgb_chroma = make_rgb_u8(px_chroma); + *color_counts.entry(rgb_chroma).or_insert(0) += 1; + } + + if current_x + 1 < width { + error_matrix[current_y][current_x + 1] += error * 7.0 / 16.0; + } + if current_y + 1 < height { + if current_x > 0 { + error_matrix[current_y + 1][current_x - 1] += error * 3.0 / 16.0; + } + error_matrix[current_y + 1][current_x] += error * 5.0 / 16.0; + if current_x + 1 < width { + error_matrix[current_y + 1][current_x + 1] += error * 1.0 / 16.0; + } + } } } - last_fg = fg; - last_bg = bg; + let fg_color = color_counts + .iter() + .max_by_key(|entry| entry.1) + .map(|(color, _)| *color) + .unwrap_or([0, 0, 0]); + let braille_char = char::from_u32(braille_char).unwrap_or(' '); + + if first || last_fg != fg_color { + out.push_str(&format!( + "\x1b[38;2;{};{};{}m{}", + fg_color[0], fg_color[1], fg_color[2], braille_char + )); + } else { + out.push(braille_char); + } + last_fg = fg_color; + first = false; } + out.push_str("\x1b[0m\n"); + } + out.trim_end().to_string() +} - out.push_str("\x0f"); - if y != image.halfblock.len() - 1 { - out.push_str("\n"); +pub fn ansi_draw_braille_8bit( + image_luma: &AnsiImage, + image_chroma: &AnsiImage, + args: &args::Args, +) -> String { + let mut out: String = String::new(); + let bitmap_luma = &image_luma.bitmap; + let bitmap_chroma = &image_chroma.bitmap; + let height = bitmap_luma.len(); + let width = bitmap_luma[0].len(); + + let mut error_matrix = vec![vec![0.0; width]; height]; + + let mut min_luma = 255u32; + let mut max_luma = 0u32; + for row in bitmap_luma.iter() { + for &pixel in row.iter() { + let l = luma(&make_rgb_u8(pixel)) as u32; + min_luma = min_luma.min(l); + max_luma = max_luma.max(l); } } - return out -} \ No newline at end of file + + let luma_range = max_luma.saturating_sub(min_luma).max(1); + + let scaled_threshold = min_luma + (luma_range / 2); + + for y in (0..height).step_by(4) { + let mut last_fg = 255u8; // Initialize with a default color + let mut first = true; + for x in (0..width).step_by(2) { + let mut braille_char = 0x2800; // Base Unicode braille character + let mut color_counts: HashMap = HashMap::new(); // Color counting + + for &(dx, dy, bit) in &POSITIONS { + let current_y = y + dy; + let current_x = x + dx; + if current_y < height && current_x < width { + let original_pixel = bitmap_luma[current_y][current_x]; + let rgb = make_rgb_u8(original_pixel); + let current_luma = luma(&rgb) as f64; + + let mut pixel_luma = current_luma + error_matrix[current_y][current_x]; + pixel_luma = pixel_luma.clamp(0.0, 255.0); + + let new_pixel = if pixel_luma > scaled_threshold as f64 { + 255.0 + } else { + 0.0 + }; + + let error = pixel_luma - new_pixel; + + if new_pixel == 255.0 { + braille_char |= bit; + + let px_chroma = bitmap_chroma[current_y][current_x]; + let color = if args.nograyscale { + nearest_hex_color(px_chroma, ANSI232.to_vec()) + } else { + nearest_hex_color(px_chroma, ANSI256.to_vec()) + }; + *color_counts.entry(color).or_insert(0) += 1; + } + + if current_x + 1 < width { + error_matrix[current_y][current_x + 1] += error * 7.0 / 16.0; + } + if current_y + 1 < height { + if current_x > 0 { + error_matrix[current_y + 1][current_x - 1] += error * 3.0 / 16.0; + } + error_matrix[current_y + 1][current_x] += error * 5.0 / 16.0; + if current_x + 1 < width { + error_matrix[current_y + 1][current_x + 1] += error * 1.0 / 16.0; + } + } + } + } + + let fg_color = *color_counts + .iter() + .max_by_key(|entry| entry.1) + .map(|(color, _)| color) + .unwrap_or(&last_fg); // Default to last_fg if no colors are counted + + let braille_char = char::from_u32(braille_char).unwrap_or(' '); + + if first || last_fg != fg_color { + out.push_str(&format!("\x1b[38;5;{}m{}", fg_color, braille_char)); + } else { + out.push(braille_char); + } + last_fg = fg_color; + first = false; + } + out.push_str("\x1b[0m\n"); // Reset colors and move to the next line + } + out.trim_end().to_string() +} + +pub fn irc_draw_braille(image_luma: &AnsiImage, image_chroma: &AnsiImage, args: &args::Args) -> String { + let mut out: String = String::new(); + let bitmap_luma = &image_luma.bitmap; + let bitmap_chroma = &image_chroma.bitmap; + let height = bitmap_luma.len(); + let width = bitmap_luma[0].len(); + + let mut error_matrix = vec![vec![0.0; width]; height]; + + let mut min_luma = 255u32; + let mut max_luma = 0u32; + + for row in bitmap_luma.iter() { + for &pixel in row.iter() { + let l = luma(&make_rgb_u8(pixel)) as u32; + min_luma = min_luma.min(l); + max_luma = max_luma.max(l); + } + } + + let luma_range = max_luma.saturating_sub(min_luma).max(1); + + let scaled_threshold = min_luma + (luma_range / 2); + + for y in (0..height).step_by(4) { + let mut last_fg = 255u8; + let mut first = true; + for x in (0..width).step_by(2) { + let mut braille_char = 0x2800; + let mut color_counts = HashMap::new(); + + for &(dx, dy, bit) in &POSITIONS { + let current_y = y + dy; + let current_x = x + dx; + if current_y < height && current_x < width { + let original_pixel = bitmap_luma[current_y][current_x]; + let rgb = make_rgb_u8(original_pixel); + let current_luma = luma(&rgb) as f64; + + let mut pixel_luma = current_luma + error_matrix[current_y][current_x]; + pixel_luma = pixel_luma.clamp(0.0, 255.0); + + let new_pixel = if pixel_luma > scaled_threshold as f64 { + 255.0 + } else { + 0.0 + }; + + let error = pixel_luma - new_pixel; + + if new_pixel == 255.0 { + braille_char |= bit; + + let px_chroma = bitmap_chroma[current_y][current_x]; + let color = if args.nograyscale { + nearest_hex_color(px_chroma, RGB88.to_vec()) + } else { + nearest_hex_color(px_chroma, RGB99.to_vec()) + }; + *color_counts.entry(color).or_insert(0) += 1; + } + + if current_x + 1 < width { + error_matrix[current_y][current_x + 1] += error * 7.0 / 16.0; + } + if current_y + 1 < height { + if current_x > 0 { + error_matrix[current_y + 1][current_x - 1] += error * 3.0 / 16.0; + } + error_matrix[current_y + 1][current_x] += error * 5.0 / 16.0; + if current_x + 1 < width { + error_matrix[current_y + 1][current_x + 1] += error * 1.0 / 16.0; + } + } + } + } + + let fg_color = *color_counts + .iter() + .max_by_key(|entry| entry.1) + .map(|(color, _)| color) + .unwrap_or(&last_fg); + + let braille_char = char::from_u32(braille_char).unwrap_or(' '); + + if first || fg_color != last_fg { + out.push_str(&format!("\x03{}{}", fg_color, braille_char)); + } else { + out.push(braille_char); + } + last_fg = fg_color; + first = false; + } + out.push_str("\x0f\n"); + } + out.trim_end().to_string() +}