libeffectengine/effects/
floyd_steinberg.rs1use std::{io::Cursor, process::exit};
2use wasm_bindgen::prelude::*;
3
4use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, 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 = floydSteinberg)]
27pub fn effect() -> Vec<u8> {
28 #[cfg(not(target_arch = "wasm32"))]
29 {
30 if subcommand_help_requested() {
31 print_help();
32 exit(0);
33 }
34 }
35
36 let paths = get_paths();
37 let image_data = read_image(paths.input_path);
38
39 let image = DynamicImage::ImageRgba8(
40 image::load_from_memory(&image_data.data)
41 .expect("Failed to decode image from memory")
42 .to_rgba8(),
43 );
44
45 let pixels = image.pixels();
46 let image_width = image.width() as usize;
47
48 let dark_color_hex = std::env::args().nth(4).unwrap_or(String::from("#000000"));
49 let light_color_hex = std::env::args().nth(5).unwrap_or(String::from("#FFFFFF"));
50
51 if !is_hex_color(dark_color_hex.clone()) || !is_hex_color(light_color_hex.clone()) {
52 eprintln!("Colors must be provided in 6 part hexadecimal format (#000000).");
53 exit(64);
54 }
55
56 let dark_color = hex_to_rgb(dark_color_hex);
57 let light_color = hex_to_rgb(light_color_hex);
58
59 let mut diffusion_array: Vec<i32> = vec![0; image_width + 1];
60 let mut diff_array_for_row: Vec<i32> = vec![0; image_width + 1];
61 let mut next_diff_err: i32 = 0;
62 let mut current_row: usize = 0;
63
64 let mut new_image: ImageBuffer<Rgba<u8>, Vec<u8>> =
65 ImageBuffer::new(image_width as u32, image.height());
66
67 for (i, pixel) in pixels.enumerate() {
68 let row_check = i / image_width;
70 if row_check > current_row {
71 current_row += 1;
72 next_diff_err = 0;
73
74 std::mem::swap(&mut diff_array_for_row, &mut diffusion_array);
76
77 for v in &mut diffusion_array {
79 *v = 0;
80 }
81 }
82
83 let proper_index = i - (image_width * current_row);
84
85 let pixel_color = pixel_to_grayscale_value(pixel);
87
88 let adjusted_pixel_color =
90 pixel_color + next_diff_err + diff_array_for_row[proper_index].clamp(0, 255);
91
92 let pixel_error = if adjusted_pixel_color < 128 {
93 new_image.put_pixel(pixel.0, pixel.1, dark_color);
94
95 adjusted_pixel_color
96 } else {
97 new_image.put_pixel(pixel.0, pixel.1, light_color);
98
99 adjusted_pixel_color - 255
100 };
101
102 next_diff_err = pixel_error * 7 >> 4;
104
105 if proper_index > 0 {
108 diffusion_array[proper_index - 1] += pixel_error * 3 >> 4;
109 }
110
111 diffusion_array[proper_index] += pixel_error * 5 >> 4;
113
114 if proper_index < image_width - 1 {
117 diffusion_array[proper_index + 1] += pixel_error >> 4;
118 }
119 }
120
121 let mut cursor = Cursor::new(Vec::new());
122
123 if image_data.format == ImageFormat::Jpeg {
124 let rgb_image = DynamicImage::ImageRgba8(new_image).into_rgb8();
125 rgb_image
126 .write_to(&mut cursor, image_data.format)
127 .expect("Failed to encode JPEG");
128 } else {
129 new_image
130 .write_to(&mut cursor, image_data.format)
131 .expect("Failed to encode image");
132 }
133
134 return cursor.into_inner();
135}
136
137#[cfg(not(target_arch = "wasm32"))]
139fn print_help() {
140 println!(
141 r#"
142Floyd Steinberg Dithering Effect
143Approximates an image using only black and white pixels.
144
145USAGE:
146 effectengine-cli floyd-steinberg <INPUT_PATH> <OUTPUT_PATH> [DARK_COLOR] [LIGHT_COLOR]
147
148ARGUMENTS:
149 <INPUT_PATH> The path to an input image that should be processed.
150 <OUTPUT_PATH> The path where the resulting image should be saved.
151 Needs to include the filename.
152 [DARK_COLOR] Optional. The color that should be used for the dark
153 pixels. Specified as a full-length hexadecimal color.
154 (Default: #000000)
155 [LIGHT_COLOR] Optional. The color that should be used for the light
156 pixels. Specified as a full-length hexadecimal color.
157 (Default: #FFFFFF)
158 "#
159 );
160}