Skip to main content

libeffectengine/effects/
floyd_steinberg.rs

1use 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/// An implementation of the Floyd-Steinberg dithering algorithm. When a pixel's error is calculated, the
12/// error is diffused down to other pixels with the following pattern (X is the current pixel, the numbers
13/// are fractions of 16):
14///
15/// ```
16/// |- - -|- - -|- - -|
17/// |     |  x  |  7  |
18/// |- - -|- - -|- - -|
19/// |  3  |  5  |  1  |
20/// |- - -|- - -|- - -|
21/// ```
22///
23/// The algorithm does this by storing the diffused error in a one-dimensional array with the size equal
24/// to the width of the image. Due to the divisor being 16, which is a multiple of two, bit-shifting can
25/// be used for better performance.
26#[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        // Check if we're in a different row by now
69        let row_check = i / image_width;
70        if row_check > current_row {
71            current_row += 1;
72            next_diff_err = 0;
73
74            // First, swap the arrays so we can work with the errors from previous rows
75            std::mem::swap(&mut diff_array_for_row, &mut diffusion_array);
76
77            // Next, clear the original diffusion array so it's good to work with again
78            for v in &mut diffusion_array {
79                *v = 0;
80            }
81        }
82
83        let proper_index = i - (image_width * current_row);
84
85        // Now we start computing the actual pixel errors
86        let pixel_color = pixel_to_grayscale_value(pixel);
87
88        // Factor in the errors from previous pixels
89        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        // The error for the next pixel to be processed.
103        next_diff_err = pixel_error * 7 >> 4;
104
105        // The errors for the next row of pixels, left to right.
106        // In cases where we're on the first pixel of a row, we can't push to the bottom left pixel.
107        if proper_index > 0 {
108            diffusion_array[proper_index - 1] += pixel_error * 3 >> 4;
109        }
110
111        // This is the value for the pixel that's right below ours!
112        diffusion_array[proper_index] += pixel_error * 5 >> 4;
113
114        // Lastly, the pixel to the bottom right.
115        // In cases where we're at the last pixel of a row, this pixel doesn't exist.
116        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/// Prints the help text for this effect.
138#[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 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}