diff --git a/Cargo.toml b/Cargo.toml index f04581c..cc1c52c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "img2irc" -version = "1.0.2" +version = "1.0.4" authors = ["waveplate"] github = "https://github.com/waveplate/img2irc" edition = "2021" diff --git a/README.md b/README.md index 2924ac0..c3aa166 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ -# img2irc (1.0.2) +# img2irc (1.0.4) ![img2irc preview](https://i.imgur.com/oetHhMB.png) -img2irc is a utility which converts images to halfblock irc/ansi art, with a lot of post-processing filters +img2irc is a utility which converts images to half or quarterblock irc/ansi art, with a lot of post-processing filters -*halfblock* means that each row will contain two rows worth of pixels, effectively doubling the resolution +*halfblock* means that each row will contain two rows worth of pixels, effectively doubling the vertical resolution + +*quarterblock* (experimental) means that each row will contain two rows worth of pixels, and each column will contain two columns worth of pixels, quadrupling the resolution the `irc` mode has 99 colours, the `ansi` mode has 256, `ansi24` has 16777216 @@ -17,6 +19,7 @@ the `irc` mode has 99 colours, the `ansi` mode has 256, `ansi24` has 16777216 | `--irc` | irc render type | true | | `--ansi` | 8-bit ansi render type | false | | `--ansi24` | 24-bit ansi render type | false | +| `--qb` | use quarterblocks | false | | `-w, --width ` | output image width in columns | 50 | | `-b, --brightness=` | adjust brightness (-255 to 255) | 0 | | `-c, --contrast=` | adjust contrast (-255 to 255) | 0 | @@ -28,6 +31,7 @@ the `irc` mode has 99 colours, the `ansi` mode has 256, `ansi24` has 16777216 | `--gaussian-blur ` | gaussian blur radius | 0 | | `--oil ` | oil ("[RADIUS],[INTENSITY]") | | | `--grayscale` | converts image to black and white | +| `--nograyscale` | exclude grayscale colours from the palette | | `--halftone` | made up of small dots creating a continuous-tone illusion | | `--sepia` | brownish, aged appearance like old photographs | | `--normalize` | adjusts brightness and contrast for better image quality | diff --git a/src/args.rs b/src/args.rs index 69a38d1..29effbf 100644 --- a/src/args.rs +++ b/src/args.rs @@ -19,6 +19,10 @@ pub struct Args { #[arg(long, default_value_t = false)] pub ansi24: bool, + /// quarterblock + #[arg(long, default_value_t = false)] + pub qb: bool, + /// image width to resize to #[arg(short, long, default_value_t = 50)] pub width: u32, @@ -63,6 +67,10 @@ pub struct Args { #[arg(long, default_value_t = false)] pub grayscale: bool, + /// no grayscale + #[arg(long, default_value_t = false)] + pub nograyscale: bool, + /// halftone #[arg(long, default_value_t = false)] pub halftone: bool, diff --git a/src/draw.rs b/src/draw.rs index f983cd7..cf99f2c 100644 --- a/src/draw.rs +++ b/src/draw.rs @@ -1,8 +1,39 @@ -use crate::palette::{RGB99, ANSI256, nearest_hex_color}; - +use crate::args; +use crate::palette::{RGB99, RGB88, ANSI232, ANSI256, nearest_hex_color}; use photon_rs::PhotonImage; -const CHAR: &str ="\u{2580}"; +// █ full +const FULL: &str = "\u{2588}"; + +// ▄ down +const UP: &str = "\u{2580}"; + +// ▀ up +const DOWN: &str = "\u{2584}"; + +// ▌ 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}"; #[derive(Debug, Clone)] pub struct AnsiImage { @@ -21,17 +52,23 @@ pub struct AnsiPixelPair { pub struct AnsiPixel { pub orig: u32, pub ansi: u8, + pub ansi232: u8, pub irc: u8, + pub irc88: u8, } impl AnsiPixel { pub fn new(pixel: &u32) -> AnsiPixel { let irc = nearest_hex_color(*pixel, RGB99.to_vec()); + let irc88 = nearest_hex_color(*pixel, RGB88.to_vec()); let ansi = nearest_hex_color(*pixel, ANSI256.to_vec()); + let ansi232 = nearest_hex_color(*pixel, ANSI232.to_vec()); AnsiPixel { orig: *pixel, ansi: ansi, + ansi232: ansi232, irc: irc, + irc88: irc88, } } } @@ -61,7 +98,6 @@ impl AnsiImage { } pub fn make_rgb_u8(rgb: u32) -> [u8; 3] { - // convert u32 to r,g,b let r = (rgb >> 16) as u8; let g = (rgb >> 8) as u8; let b = rgb as u8; @@ -114,6 +150,33 @@ 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); + + 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; + + 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, + } +} + pub fn ansi_draw_24bit(image: AnsiImage) -> String { let mut out: String = String::new(); for (y, row) in image.halfblock.iter().enumerate() { @@ -130,7 +193,7 @@ pub fn ansi_draw_24bit(image: AnsiImage) -> String { .map(|x| x.to_string()) .collect::>(); - out.push_str(format!("\x1b[38;2;{}m\x1b[48;2;{}m{}", fg.join(";"), bg.join(";"), CHAR).as_str()); + out.push_str(format!("\x1b[38;2;{}m\x1b[48;2;{}m{}", fg.join(";"), bg.join(";"), UP).as_str()); } out.push_str("\x1b[0m"); @@ -141,14 +204,54 @@ pub fn ansi_draw_24bit(image: AnsiImage) -> String { return out } -pub fn ansi_draw_8bit(image: AnsiImage) -> String { +pub fn ansi_draw_24bit_qb(image: AnsiImage) -> 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 pixel_pair in row.iter() { - let fg = pixel_pair.top.ansi; - let bg = pixel_pair.bottom.ansi; + + let fg = match args.nograyscale { + true => pixel_pair.top.ansi232, + false => pixel_pair.top.ansi, + }; - out.push_str(format!("\x1b[38;5;{}m\x1b[48;5;{}m{}", fg, bg, CHAR).as_str()); + let bg = match args.nograyscale { + true => pixel_pair.bottom.ansi232, + false => 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"); @@ -159,25 +262,108 @@ pub fn ansi_draw_8bit(image: AnsiImage) -> String { return out } -pub fn irc_draw(image: AnsiImage) -> String { +pub fn ansi_draw_8bit_qb(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 (x, pixel_pair) in row.iter().enumerate() { - let fg = pixel_pair.top.irc; - let bg = pixel_pair.bottom.irc; + let fg = match args.nograyscale { + true => pixel_pair.top.irc88, + false => pixel_pair.top.irc, + }; + + let bg = match args.nograyscale { + true => pixel_pair.bottom.irc88, + false => pixel_pair.bottom.irc, + }; if x != 0 { if fg == last_fg && bg == last_bg { - out.push_str(&format!("{}", CHAR)); + out.push_str(&format!("{}", UP)); } else if bg == last_bg { - out.push_str(&format!("\x03{}{}", fg, CHAR)); + out.push_str(&format!("\x03{}{}", fg, UP)); } else { - out.push_str(&format!("\x03{},{}{}", fg, bg, CHAR)); + out.push_str(&format!("\x03{},{}{}", fg, bg, UP)); } } else { - out.push_str(&format!("\x03{},{}{}", fg, bg, CHAR)); + out.push_str(&format!("\x03{},{}{}", fg, bg, UP)); + } + + last_fg = fg; + last_bg = bg; + } + + out.push_str("\x0f"); + + if y != image.halfblock.len() - 1 { + out.push_str("\n"); + } + } + return out +} + +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() { + 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 bg = match args.nograyscale { + true => pixel_pairs[0].bottom.irc88, + false => pixel_pairs[0].bottom.irc, + }; + + let char = match y { + _ if y == image.halfblock.len() - 1 => UP, + _ => get_qb_char(pixel_pairs), + }; + + 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)); + } } last_fg = fg; diff --git a/src/effects.rs b/src/effects.rs index cf8136c..7734454 100644 --- a/src/effects.rs +++ b/src/effects.rs @@ -13,7 +13,15 @@ pub fn apply_effects( let height = (args.width as f32 / photon_image.get_width() as f32 * photon_image.get_height() as f32) as u32; - photon_image = resize(&mut photon_image, args.width, height, SamplingFilter::Lanczos3); + let width = match args.qb { + true => args.width * 2, + _ => args.width, + }; + + photon_image = match args.qb { + true => resize(&photon_image, width, height, SamplingFilter::Lanczos3), + _ => resize(&mut photon_image, width, height, SamplingFilter::Lanczos3), + }; // Adjust brightness match args.brightness { diff --git a/src/main.rs b/src/main.rs index e34e26a..7910da2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,15 +23,16 @@ async fn main() { let canvas = draw::AnsiImage::new(image); - if args.irc { - println!("{}", draw::irc_draw(canvas).as_str()); - } else if args.ansi { - println!("{}", draw::ansi_draw_8bit(canvas).as_str()); - } else if args.ansi24 { - println!("{}", draw::ansi_draw_24bit(canvas).as_str()); - } else { - println!("{}", draw::irc_draw(canvas).as_str()); - } + match (args.irc, args.ansi, args.ansi24, args.qb) { + (true, _, _, true) => println!("{}", draw::irc_draw_qb(canvas, &args).as_str()), + (true, _, _, false) => println!("{}", draw::irc_draw(canvas, &args).as_str()), + (_, true, _, true) => println!("{}", draw::ansi_draw_8bit_qb(canvas, &args).as_str()), + (_, true, _, false) => println!("{}", draw::ansi_draw_8bit(canvas, &args).as_str()), + (_, _, true, true) => println!("{}", draw::ansi_draw_24bit_qb(canvas).as_str()), + (_, _, true, false) => println!("{}", draw::ansi_draw_24bit(canvas).as_str()), + (_, _, _, true) => println!("{}", draw::irc_draw_qb(canvas, &args).as_str()), + _ => println!("{}", draw::irc_draw(canvas, &args).as_str()), + } } Err(e) => { diff --git a/src/palette.rs b/src/palette.rs index 91371f4..8a7ec73 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -1,3 +1,17 @@ +pub const RGB88: [u32; 88] = [ + 0xffffff, 0x000000, 0x00007f, 0x009300, 0xff0000, 0x7f0000, 0x9c009c, 0xfc7f00, + 0xffff00, 0x00fc00, 0x009393, 0x00ffff, 0x0000fc, 0xff00ff, 0x0, 0x0, + 0x470000, 0x472100, 0x474700, 0x324700, 0x004700, 0x00472c, 0x004747, 0x002747, + 0x000047, 0x2e0047, 0x470047, 0x47002a, 0x740000, 0x743a00, 0x747400, 0x517400, + 0x007400, 0x007449, 0x007474, 0x004074, 0x000074, 0x4b0074, 0x740074, 0x740045, + 0xb50000, 0xb56300, 0xb5b500, 0x7db500, 0x00b500, 0x00b571, 0x00b5b5, 0x0063b5, + 0x0000b5, 0x7500b5, 0xb500b5, 0xb5006b, 0xff0000, 0xff8c00, 0xffff00, 0xb2ff00, + 0x00ff00, 0x00ffa0, 0x00ffff, 0x008cff, 0x0000ff, 0xa500ff, 0xff00ff, 0xff0098, + 0xff5959, 0xffb459, 0xffff71, 0xcfff60, 0x6fff6f, 0x65ffc9, 0x6dffff, 0x59b4ff, + 0x5959ff, 0xc459ff, 0xff66ff, 0xff59bc, 0xff9c9c, 0xffd39c, 0xffff9c, 0xe2ff9c, + 0x9cff9c, 0x9cffdb, 0x9cffff, 0x9cd3ff, 0x9c9cff, 0xdc9cff, 0xff9cff, 0xff94d3, +]; + pub const RGB99: [u32; 99] = [ 0xffffff, 0x000000, 0x00007f, 0x009300, 0xff0000, 0x7f0000, 0x9c009c, 0xfc7f00, 0xffff00, 0x00fc00, 0x009393, 0x00ffff, 0x0000fc, 0xff00ff, 0x7f7f7f, 0xd2d2d2, @@ -49,6 +63,38 @@ pub const ANSI256: [u32; 256] = [ 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee ]; +pub const ANSI232: [u32; 232] = [ + 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0x0, + 0x0, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0x0, + 0x0, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, + 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, + 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, + 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, + 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, + 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x0, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, + 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, + 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, + 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, + 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, + 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x0, 0x8787af, + 0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, + 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, + 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, + 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, + 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, + 0xafaf87, 0x0, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, + 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, + 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, + 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, + 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, + 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0x0, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, + 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, + 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, + 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, + 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, + 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, +]; + fn hex_to_rgb(hex: u32) -> (u8, u8, u8) { let r = ((hex >> 16) & 0xFF) as u8; let g = ((hex >> 8) & 0xFF) as u8;