Skip to main content

libeffectengine/effects/
quantize.rs

1use 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/// "Quantizes" an image by adjusting the colors to fit a given palette.
12/// Each pixel's color is checked for the lowest perceived distance to
13/// the color palette, then that new color is written to the new image
14/// instead.
15#[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
65/// Finds the closest color for a given pixel from a given palette.
66fn 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
90/// Collects the palette colors, either from an input image or from CLI input args.
91fn 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        // Check if the 5th arg is a path to a pallete image, otherwise throw an error saying that more than one color is required
140    } 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/// Prints the help text for this effect.
163#[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}