first commit

This commit is contained in:
Anatoly Bazarov 2023-03-29 23:04:36 -07:00
commit 8c191f264e
7 changed files with 716 additions and 0 deletions

13
Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "img2irc"
version = "0.1.0"
authors = ["anatolybazarov"]
github = "https://github.com/anatolybazarov/img2irc"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
reqwest = "0.11.14"
photon-rs = "0.3.2"
clap = { version = "4.2.0", features = ["cargo", "derive"] }
url = "2.3.1"

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# img2irc (0.1.0)
![EVA Loader](https://i.imgur.com/wLyj1HH.png)
img2irc is a utility which converts images to halfblock 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
the `irc` mode has 99 colours, the `ansi` mode has 256
## usage
`./img2ansi <URL or PATH> [OPTIONS]`
| option | description | default value |
| ------ | ----------- | ------------- |
| `<IMAGE>` | image url or file path | none |
| `-r, --render <RENDER>` | render type (irc, ansi) | irc |
| `-w, --width <WIDTH>` | output image width in columns | 50 |
| `-b, --brightness=<BRIGHTNESS>` | adjust brightness (-255 to 255) | 0 |
| `-H, --hue=<HUE>` | adjust hue (-180 to 180) | 0 |
| `-c, --contrast=<CONTRAST>` | adjust contrast (-255 to 255) | 0 |
| `-s, --saturation=<SATURATION>` | adjust saturation (-255 to 255) | 0 |
| `-o, --opacity=<OPACITY>` | adjust opacity (-255 to 255) | 0 |
| `-g, --gamma=<GAMMA>` | adjust gamma (-255 to 255) | 0 |
| `--dither <DITHER>` | dithering (1 to 8) | 0 |
| `--pixelize <PIXELIZE>` | pixelize pixel size | 0 |
| `--gaussian-blur <GAUSSIAN_BLUR>` | gaussian blur radius | 0 |
| `--oil <OIL>` | oil ("[RADIUS],[INTENSITY]") | |
| `--grayscale` | converts image to black and white |
| `--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 |
| `--noise` | random variations in brightness and color like film grain |
| `--emboss` | gives a raised, 3d appearance |
| `--box-blur` | smoothed appearance like frosted glass |
| `--identity` | no modifications, unchanged image |
| `--laplace` | enhances edges and boundaries in an image |
| `--noise-reduction` | reduces noise for a cleaner, clearer image |
| `--sharpen` | increases clarity and definition, making edges and details more distinct |
| `--cali` | cool blue tone with increased contrast |
| `--dramatic` | high contrast and vivid colors for a dramatic effect |
| `--firenze` | warm, earthy tones reminiscent of tuscan landscapes |
| `--golden` | warm, golden glow like sunset light |
| `--lix` | high-contrast black and white appearance with increased sharpness |
| `--lofi` | low-fidelity, retro appearance like old photographs or film |
| `--neue` | clean, modern appearance with neutral colors and simple design |
| `--obsidian` | dark, monochromatic appearance with black and gray shades |
| `--pastel-pink` | soft, delicate pink tint like pastel colors |
| `--ryo` | bright, high-contrast appearance with vivid colors and sharp details |
| `--invert` | colors are inverted, opposite on the color wheel |
| `--frosted-glass` | blurred, frosted appearance as if viewed through semi-transparent surface |
| `--solarize` | strange, otherworldly appearance with inverted colors and surreal atmosphere |
| `--edge-detection` | highlights edges and boundaries in an image |

161
src/args.rs Normal file
View File

@ -0,0 +1,161 @@
use clap::Parser;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
/// image url or file path
#[arg(index = 1)]
pub image: String,
/// render type (irc, ansi)
#[arg(short, long)]
pub render: Option<String>,
/// image width to resize to
#[arg(short, long, default_value_t = 50)]
pub width: u32,
/// brightness (-255 to 255)
#[arg(short, long, require_equals = true, default_value_t = 0)]
pub brightness: i16,
/// hue (-180 to 180)
#[arg(short = 'H', long, require_equals = true, default_value_t = 0)]
pub hue: i16,
/// contrast (-255 to 255)
#[arg(short, long, require_equals = true, default_value_t = 0)]
pub contrast: i16,
/// saturation (-255 to 255)
#[arg(short, long, require_equals = true, default_value_t = 0)]
pub saturation: i16,
/// opacity (-255 to 255)
#[arg(short, long, require_equals = true, default_value_t = 0)]
pub opacity: i16,
/// gamma (-255 to 255)
#[arg(short, long, require_equals = true, default_value_t = 0)]
pub gamma: i16,
/// dither (1 to 8)
#[arg(long, default_value_t = 0)]
pub dither: u32,
/// pixelize size
#[arg(long, default_value_t = 0)]
pub pixelize: i32,
/// gaussian blur radius
#[arg(long, default_value_t = 0)]
pub gaussian_blur: i32,
/// oil ("<radius>,<intensity>")
#[arg(long)]
pub oil: Option<String>,
/// grayscale
#[arg(long, default_value_t = false)]
pub grayscale: bool,
/// halftone
#[arg(long, default_value_t = false)]
pub halftone: bool,
/// sepia
#[arg(long, default_value_t = false)]
pub sepia: bool,
/// normalize
#[arg(long, default_value_t = false)]
pub normalize: bool,
/// noise
#[arg(long, default_value_t = false)]
pub noise: bool,
/// emboss
#[arg(long, default_value_t = false)]
pub emboss: bool,
/// box_blur
#[arg(long, default_value_t = false)]
pub box_blur: bool,
/// identity
#[arg(long, default_value_t = false)]
pub identity: bool,
/// laplace
#[arg(long, default_value_t = false)]
pub laplace: bool,
/// noise reduction
#[arg(long, default_value_t = false)]
pub noise_reduction: bool,
/// sharpen
#[arg(long, default_value_t = false)]
pub sharpen: bool,
/// cali
#[arg(long, default_value_t = false)]
pub cali: bool,
/// dramatic
#[arg(long, default_value_t = false)]
pub dramatic: bool,
/// firenze
#[arg(long, default_value_t = false)]
pub firenze: bool,
/// golden
#[arg(long, default_value_t = false)]
pub golden: bool,
/// lix
#[arg(long, default_value_t = false)]
pub lix: bool,
/// lofi
#[arg(long, default_value_t = false)]
pub lofi: bool,
/// neue
#[arg(long, default_value_t = false)]
pub neue: bool,
/// obsidian
#[arg(long, default_value_t = false)]
pub obsidian: bool,
/// pastel_pink
#[arg(long, default_value_t = false)]
pub pastel_pink: bool,
/// ryo
#[arg(long, default_value_t = false)]
pub ryo: bool,
/// invert
#[arg(long, default_value_t = false)]
pub invert: bool,
/// frosted glass
#[arg(long, default_value_t = false)]
pub frosted_glass: bool,
/// solarize
#[arg(long, default_value_t = false)]
pub solarize: bool,
/// edge detection
#[arg(long, default_value_t = false)]
pub edge_detection: bool,
}
pub fn parse_args() -> Args {
Args::parse()
}

145
src/draw.rs Normal file
View File

@ -0,0 +1,145 @@
use crate::palette::{RGB99, ANSI256, nearest_hex_color};
use photon_rs::PhotonImage;
const CHAR: &str ="\u{2580}";
#[derive(Debug, Clone)]
pub struct AnsiImage {
pub image: PhotonImage,
pub bitmap: Vec<Vec<u32>>,
pub halfblock: Vec<Vec<AnsiPixelPair>>,
}
#[derive(Debug, Clone, Copy)]
pub struct AnsiPixelPair {
pub top: AnsiPixel,
pub bottom: AnsiPixel,
}
#[derive(Debug, Clone, Copy)]
pub struct AnsiPixel {
pub orig: u32,
pub ansi: u8,
pub irc: u8,
}
impl AnsiPixel {
pub fn new(pixel: &u32) -> AnsiPixel {
let irc = nearest_hex_color(*pixel, RGB99.to_vec());
let ansi = nearest_hex_color(*pixel, ANSI256.to_vec());
AnsiPixel {
orig: *pixel,
ansi: ansi,
irc: irc,
}
}
}
impl AnsiImage {
pub fn new(image: PhotonImage) -> AnsiImage {
let mut bitmap = image.get_raw_pixels()
.chunks(4)
.map(|x| make_rgb(x.to_vec()))
.collect::<Vec<u32>>()
.chunks(image.get_width() as usize)
.map(|x| x.to_vec())
.collect::<Vec<Vec<u32>>>();
if bitmap.len() % 2 != 0 {
bitmap.push(vec![0; image.get_width() as usize]);
}
let halfblock = halfblock_bitmap(&bitmap);
return AnsiImage {
image: image,
bitmap: bitmap,
halfblock: halfblock,
}
}
}
pub fn make_rgb(rgb: Vec<u8>) -> 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 rgb = (r << 16) + (g << 8) + b;
return rgb
}
pub fn halfblock_bitmap(bitmap: &Vec<Vec<u32>>) -> Vec<Vec<AnsiPixelPair>> {
let ansi_bitmap = bitmap
.iter()
.map(|x| {
x.iter().map(|y| AnsiPixel::new(y)).collect::<Vec<AnsiPixel>>()
})
.collect::<Vec<Vec<AnsiPixel>>>();
let mut ansi_canvas: Vec<Vec<AnsiPixelPair>> = 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 mut ansi_row: Vec<AnsiPixelPair> = 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();
let pixel_pair = AnsiPixelPair {
top: *top_pixel,
bottom: *bottom_pixel,
};
ansi_row.push(pixel_pair);
}
ansi_canvas.push(ansi_row);
}
ansi_canvas
}
pub fn ansi_draw(image: AnsiImage) -> String {
let mut out: String = String::new();
for row in image.halfblock {
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());
}
out.push_str("\x1b[0m");
out.push_str("\n");
}
return out
}
pub fn irc_draw(image: AnsiImage) -> String {
let mut out: String = String::new();
for row in image.halfblock {
let mut last_fg: u8 = 0;
let mut last_bg: u8 = 0;
for pixel_pair in row.iter() {
let fg = pixel_pair.top.irc;
let bg = pixel_pair.bottom.irc;
if fg == last_fg && bg == last_bg {
out.push_str(format!("{}", CHAR).as_str());
} else if bg == last_bg {
out.push_str(format!("\x03{}{}", fg, CHAR).as_str());
} else {
out.push_str(format!("\x03{},{}{}", fg, bg, CHAR).as_str());
}
last_fg = fg;
last_bg = bg;
}
out.push_str("\n");
}
return out
}

202
src/effects.rs Normal file
View File

@ -0,0 +1,202 @@
use crate::args;
use photon_rs::{channels, channels::alter_channels, conv, effects, filters, monochrome, noise};
use photon_rs::transform::{resize, SamplingFilter};
use photon_rs::PhotonImage;
pub fn apply_effects(
args: &args::Args,
mut photon_image: PhotonImage,
) -> PhotonImage {
// Resize to width
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);
// Adjust brightness
if args.brightness != 0 {
alter_channels(&mut photon_image, args.brightness, args.brightness, args.brightness);
}
// Adjust hue
if args.hue != 0 {
alter_channels(&mut photon_image, args.hue, args.hue, args.hue);
}
// Adjust contrast
if args.contrast != 0 {
alter_channels(&mut photon_image, args.contrast, args.contrast, args.contrast);
}
// Adjust saturation
if args.saturation != 0 {
alter_channels(&mut photon_image, args.saturation, args.saturation, args.saturation);
}
// Adjust opacity
if args.opacity != 0 {
alter_channels(&mut photon_image, args.opacity, args.opacity, args.opacity);
}
// Adjust gamma
if args.gamma != 0 {
alter_channels(&mut photon_image, args.gamma, args.gamma, args.gamma);
}
// Adjust dither
if args.dither > 0 {
effects::dither(&mut photon_image, args.dither);
}
// Adjust gaussian_blur
if args.gaussian_blur > 0 {
conv::gaussian_blur(&mut photon_image, args.gaussian_blur);
}
// Adjust pixelize
if args.pixelize > 0 {
effects::pixelize(&mut photon_image, args.pixelize);
}
// Adjust halftone
if args.halftone {
effects::halftone(&mut photon_image);
}
// Adjust invert
if args.invert {
channels::invert(&mut photon_image);
}
// Adjust sepia
if args.sepia {
monochrome::sepia(&mut photon_image);
}
// Adjust solarize
if args.solarize {
effects::solarize(&mut photon_image);
}
// Adjust normalize
if args.normalize {
effects::normalize(&mut photon_image);
}
// Adjust noise
if args.noise {
noise::add_noise_rand(photon_image.clone());
}
// Adjust sharpen
if args.sharpen {
conv::sharpen(&mut photon_image);
}
// Adjust edge_detection
if args.edge_detection {
conv::edge_detection(&mut photon_image);
}
// Adjust emboss
if args.emboss {
conv::emboss(&mut photon_image);
}
// Adjust frosted_glass
if args.frosted_glass {
effects::frosted_glass(&mut photon_image);
}
// Adjust box_blur
if args.box_blur {
conv::box_blur(&mut photon_image);
}
// Adjust grayscale
if args.grayscale {
monochrome::grayscale(&mut photon_image);
}
// Adjust identity
if args.identity {
conv::identity(&mut photon_image);
}
// Adjust laplace
if args.laplace {
conv::laplace(&mut photon_image);
}
// Adjust cali
if args.cali {
filters::cali(&mut photon_image);
}
// Adjust dramatic
if args.dramatic {
filters::dramatic(&mut photon_image);
}
// Adjust firenze
if args.firenze {
filters::firenze(&mut photon_image);
}
// Adjust golden
if args.golden {
filters::golden(&mut photon_image);
}
// Adjust lix
if args.lix {
filters::lix(&mut photon_image);
}
// Adjust lofi
if args.lofi {
filters::lofi(&mut photon_image);
}
// Adjust neue
if args.neue {
filters::neue(&mut photon_image);
}
// Adjust obsidian
if args.obsidian {
filters::obsidian(&mut photon_image);
}
// Adjust pastel_pink
if args.pastel_pink {
filters::pastel_pink(&mut photon_image);
}
// Adjust ryo
if args.ryo {
filters::ryo(&mut photon_image);
}
// Adjust oil
match &args.oil {
Some(oil) => {
// split oil at comma
let vals: Vec<&str> = oil.split(",").collect();
// check if args.oil has 2 values
if vals.len() == 2 {
// convert oil values to i32 and f64
let radius: i32 = vals.get(0).unwrap().parse::<i32>().unwrap();
let intensity: f64 = vals.get(1).unwrap().parse::<f64>().unwrap();
effects::oil(&mut photon_image, radius, intensity);
}
}
None => {}
}
photon_image
}

64
src/main.rs Normal file
View File

@ -0,0 +1,64 @@
mod args;
mod draw;
mod palette;
mod effects;
use reqwest;
use url::Url;
use photon_rs::PhotonImage;
use std::{error::Error, io::Cursor, process::exit};
#[tokio::main]
async fn main() {
let args = args::parse_args();
match load_image_from_url_or_path(args.image.as_str()).await {
Ok(mut image) => {
image = effects::apply_effects(
&args,
image,
);
let canvas = draw::AnsiImage::new(image);
match &args.render {
None => println!("{}", draw::irc_draw(canvas).as_str()),
Some(ref render) => match render.as_str() {
"irc" => println!("{}", draw::irc_draw(canvas).as_str()),
"ansi" => println!("{}", draw::ansi_draw(canvas).as_str()),
_ => {
eprintln!("Error: invalid render type");
exit(1);
}
},
}
}
Err(e) => {
eprintln!("Error: {}", e);
exit(1);
}
}
}
async fn load_image_from_url_or_path(image: &str) -> Result<PhotonImage, Box<dyn Error>> {
match Url::parse(image) {
Ok(url) => {
let response = reqwest::get(url).await?;
let bytes = response.bytes().await?;
let image_data = Cursor::new(bytes);
match photon_rs::native::open_image_from_bytes(image_data.into_inner().as_ref()) {
Ok(image) => Ok(image),
Err(e) => Err(Box::new(e)),
}
}
Err(_) => {
match photon_rs::native::open_image(image) {
Ok(image) => Ok(image),
Err(e) => Err(Box::new(e)),
}
}
}
}

78
src/palette.rs Normal file
View File

@ -0,0 +1,78 @@
pub const RGB99: [u32; 99] = [
0xffffff, 0x000000, 0x00007f, 0x009300, 0xff0000, 0x7f0000, 0x9c009c, 0xfc7f00,
0xffff00, 0x00fc00, 0x009393, 0x00ffff, 0x0000fc, 0xff00ff, 0x7f7f7f, 0xd2d2d2,
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,
0x000000, 0x131313, 0x282828, 0x363636, 0x4d4d4d, 0x656565, 0x818181, 0x9f9f9f,
0xbcbcbc, 0xe2e2e2, 0xffffff,
];
pub const ANSI256: [u32; 256] = [
0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0,
0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff,
0x000000, 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, 0x5f5f5f, 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, 0x878787, 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, 0xafafaf, 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, 0xd7d7d7, 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,
0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e,
0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e,
0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee
];
fn hex_to_rgb(hex: u32) -> (u8, u8, u8) {
let r = ((hex >> 16) & 0xFF) as u8;
let g = ((hex >> 8) & 0xFF) as u8;
let b = (hex & 0xFF) as u8;
(r, g, b)
}
fn color_distance_squared(c1: (u8, u8, u8), c2: (u8, u8, u8)) -> u32 {
let dr = c1.0 as i32 - c2.0 as i32;
let dg = c1.1 as i32 - c2.1 as i32;
let db = c1.2 as i32 - c2.2 as i32;
(dr * dr + dg * dg + db * db) as u32
}
pub fn nearest_hex_color(input_color: u32, hex_colors: Vec<u32>) -> u8 {
let hex = hex_colors
.iter()
.map(|&hex| (hex, hex_to_rgb(hex)))
.min_by_key(|(_, rgb)| color_distance_squared(hex_to_rgb(input_color), *rgb))
.map(|(hex, _)| hex).unwrap();
let index = hex_colors.iter().position(|&x| x == hex).unwrap();
return index as u8;
}