libeffectengine/effects/
quantize.rs1use std::{collections::HashMap, io::Cursor, path::PathBuf, process::exit};
2use wasm_bindgen::prelude::*;
3
4use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, ImageReader, Rgba};
5
6use crate::util::{get_paths, hex_to_rgb, is_hex_color, pixel_to_grayscale_value, read_image};
7
8#[cfg(not(target_arch = "wasm32"))]
9use crate::util::subcommand_help_requested;
10
11#[wasm_bindgen(js_name = quantize)]
16pub fn effect() -> Vec<u8> {
17 #[cfg(not(target_arch = "wasm32"))]
18 {
19 if subcommand_help_requested() {
20 print_help();
21 exit(0);
22 }
23 }
24
25 let paths = get_paths();
26 let image_data = read_image(paths.input_path);
27
28 let image = DynamicImage::ImageRgba8(
29 image::load_from_memory(&image_data.data)
30 .expect("Failed to decode image from memory")
31 .to_rgba8(),
32 );
33 let image_width = image.width();
34 let image_height = image.height();
35
36 let colors = collect_palette_colors();
37 let mut cache: HashMap<[u8; 4], Rgba<u8>> = HashMap::new();
38
39 let mut new_image = ImageBuffer::new(image_width, image_height);
40
41 for (x, y, pixel) in image.pixels() {
42 let quantized_color = *cache
43 .entry(pixel.0)
44 .or_insert_with(|| find_closest_color(pixel, &colors));
45
46 new_image.put_pixel(x, y, quantized_color);
47 }
48
49 let mut cursor = Cursor::new(Vec::new());
50
51 if image_data.format == ImageFormat::Jpeg {
52 let rgb_image = DynamicImage::ImageRgba8(new_image).into_rgb8();
53 rgb_image
54 .write_to(&mut cursor, image_data.format)
55 .expect("Failed to encode JPEG");
56 } else {
57 new_image
58 .write_to(&mut cursor, image_data.format)
59 .expect("Failed to encode image");
60 }
61
62 return cursor.into_inner();
63}
64
65fn find_closest_color(pixel: Rgba<u8>, palette: &Vec<Rgba<u8>>) -> Rgba<u8> {
67 let r1 = pixel.0[0] as f32;
68 let g1 = pixel.0[1] as f32;
69 let b1 = pixel.0[2] as f32;
70
71 let mut min_dist = f32::MAX;
72 let mut closest_color = palette[0];
73
74 for color in palette {
75 let dr = r1 - color[0] as f32;
76 let dg = g1 - color[1] as f32;
77 let db = b1 - color[2] as f32;
78
79 let dist_sq = 0.299 * dr * dr + 0.587 * dg * dg + 0.114 * db * db;
80
81 if dist_sq < min_dist {
82 min_dist = dist_sq;
83 closest_color = *color;
84 }
85 }
86
87 closest_color
88}
89
90fn collect_palette_colors() -> Vec<Rgba<u8>> {
92 let mut colors: Vec<Rgba<u8>> = Vec::new();
93
94 if std::env::args().len() < 5
95 || std::env::args().len() == 5 && is_hex_color(std::env::args().nth(4).unwrap())
96 {
97 eprintln!("Need at least two colors or a path to a palette image.");
98 exit(64);
99 }
100
101 if std::env::args().len() == 5 {
102 let palette_path = PathBuf::from(std::env::args().nth(4).unwrap());
103
104 if !palette_path.exists() {
105 eprintln!("The given palette was not found.");
106 exit(64);
107 }
108
109 if !palette_path.is_file() {
110 eprintln!("The given palette was not a file.");
111 exit(64);
112 }
113
114 let palette_reader_res = ImageReader::open(palette_path);
115
116 let palette_reader = match palette_reader_res {
117 Ok(_) => palette_reader_res.unwrap(),
118 Err(_) => {
119 eprintln!("The image at the given input path could not be read.");
120 exit(64);
121 }
122 };
123
124 let palette_image_res = palette_reader.decode();
125
126 let palette_image = match palette_image_res {
127 Ok(_) => palette_image_res.unwrap(),
128 Err(_) => {
129 eprintln!("The palette image at the given path could not be decoded.");
130 exit(64);
131 }
132 };
133
134 for pixel in palette_image.pixels() {
135 if colors.iter().find(|&x| *x == pixel.2).is_none() {
136 colors.push(pixel.2);
137 }
138 }
139 } else {
141 for (i, arg) in std::env::args().enumerate() {
142 if i < 4 {
143 continue;
144 }
145
146 if !is_hex_color(arg.clone()) {
147 eprintln!(
148 "All arguments after the file paths must be colors in full hexadecimal format (#000000)!"
149 );
150 exit(64);
151 }
152
153 colors.push(hex_to_rgb(arg));
154 }
155 }
156
157 colors.sort_by_key(|c| pixel_to_grayscale_value((0, 0, *c)));
158
159 colors
160}
161
162#[cfg(not(target_arch = "wasm32"))]
164fn print_help() {
165 println!(
166 r#"
167Quantization Effect
168"Quantizes" an image by adjusting the colors to fit a given palette.
169
170USAGE:
171 effectengine quantize <INPUT_PATH> <OUTPUT_PATH> [PALETTE_PATH | HEX_CODES...]
172
173ARGUMENTS:
174 <INPUT_PATH> The path to an input image that should be processed.
175 <OUTPUT_PATH> The path where the resulting image should be saved.
176 Needs to include the filename.
177 [PALETTE_PATH] A path to an image, the colors of which should be used as
178 the base palette for the conversion. A good source for
179 palettes is https://lospec.com/palette-list!
180 [HEX_CODES...] A list of hex codes in full format (e.g. #000000 or
181 #FFFFFF). Minimum two.
182 "#
183 );
184}