add quarterblock and nograyscale mode

This commit is contained in:
Anatoly Bazarov 2023-06-27 23:46:08 -07:00
parent 715dfe5dac
commit b29c009c42
7 changed files with 283 additions and 30 deletions

View File

@ -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"

View File

@ -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 <WIDTH>` | output image width in columns | 50 |
| `-b, --brightness=<BRIGHTNESS>` | adjust brightness (-255 to 255) | 0 |
| `-c, --contrast=<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>` | gaussian blur radius | 0 |
| `--oil <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 |

View File

@ -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,

View File

@ -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<u32>>) -> Vec<Vec<AnsiPixelPair>> {
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::<Vec<String>>();
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::<Vec<String>>();
let bg = make_rgb_u8(pixel_pairs[0].bottom.orig)
.to_vec()
.iter()
.map(|x| x.to_string())
.collect::<Vec<String>>();
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;
out.push_str(format!("\x1b[38;5;{}m\x1b[48;5;{}m{}", fg, bg, CHAR).as_str());
let fg = match args.nograyscale {
true => pixel_pair.top.ansi232,
false => pixel_pair.top.ansi,
};
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;

View File

@ -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 {

View File

@ -23,14 +23,15 @@ 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()),
}
}

View File

@ -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;