Skip to main content

libeffectengine/effects/
pixel_sort.rs

1use std::io::Cursor;
2#[cfg(not(target_arch = "wasm32"))]
3use std::process::exit;
4use wasm_bindgen::prelude::*;
5
6use image::{DynamicImage, GenericImageView, ImageBuffer, ImageFormat, Rgba};
7
8use crate::util::{get_paths, pixel_to_grayscale_value, read_image};
9
10#[cfg(not(target_arch = "wasm32"))]
11use crate::util::subcommand_help_requested;
12
13/// Sorts all pixels in an image above the image's average brightness
14/// by their brightness value. Results in a tearing-like effect often
15/// used in video games like Cyberpunk 2077.
16///
17/// First, the average brightness is calculated. Afterwards, given
18/// a mode (horizontal, vertical or both), pixels lighter than the
19/// average brightness are prepared to be sorted, then sorted in
20/// intervals.
21#[wasm_bindgen(js_name = pixelSort)]
22pub fn effect() -> Vec<u8> {
23    #[cfg(not(target_arch = "wasm32"))]
24    {
25        if subcommand_help_requested() {
26            print_help();
27            exit(0);
28        }
29    }
30
31    let paths = get_paths();
32    let image_data = read_image(paths.input_path);
33
34    let image = DynamicImage::ImageRgba8(
35        image::load_from_memory(&image_data.data)
36            .expect("Failed to decode image from memory")
37            .to_rgba8(),
38    );
39    let image_width = image.width();
40    let image_height = image.height();
41
42    let mut new_image = ImageBuffer::new(image_width, image_height);
43
44    let total_brightness = image.pixels().fold(0, |acc, pixel| {
45        let grayscale = pixel_to_grayscale_value(pixel) as usize;
46
47        acc + grayscale
48    });
49
50    let avg_brightness = total_brightness / (image_width as usize * image_height as usize);
51
52    let mode = std::env::args()
53        .nth(4)
54        .or_else(|| Some(String::from("horizontal")))
55        .unwrap();
56
57    match mode.as_str() {
58        "vertical" => {
59            let (pixel_positions, pixels_to_be_sorted) =
60                get_vertical_pixels_to_be_sorted(&image, &mut new_image, avg_brightness);
61            sort_pixels(&mut new_image, pixel_positions, pixels_to_be_sorted);
62        }
63        "both" => {
64            let (mut pixel_positions, mut pixels_to_be_sorted) =
65                get_horizontal_pixels_to_be_sorted(&image, &mut new_image, avg_brightness);
66            sort_pixels(&mut new_image, pixel_positions, pixels_to_be_sorted);
67
68            let new_image_base = DynamicImage::ImageRgba8(new_image.clone());
69            (pixel_positions, pixels_to_be_sorted) =
70                get_vertical_pixels_to_be_sorted(&new_image_base, &mut new_image, avg_brightness);
71            sort_pixels(&mut new_image, pixel_positions, pixels_to_be_sorted);
72        }
73        "horizontal" => {
74            let (pixel_positions, pixels_to_be_sorted) =
75                get_horizontal_pixels_to_be_sorted(&image, &mut new_image, avg_brightness);
76            sort_pixels(&mut new_image, pixel_positions, pixels_to_be_sorted);
77        }
78        _ => {
79            let (pixel_positions, pixels_to_be_sorted) =
80                get_horizontal_pixels_to_be_sorted(&image, &mut new_image, avg_brightness);
81            sort_pixels(&mut new_image, pixel_positions, pixels_to_be_sorted);
82        }
83    }
84
85    let mut cursor = Cursor::new(Vec::new());
86
87    if image_data.format == ImageFormat::Jpeg {
88        let rgb_image = DynamicImage::ImageRgba8(new_image).into_rgb8();
89        rgb_image
90            .write_to(&mut cursor, image_data.format)
91            .expect("Failed to encode JPEG");
92    } else {
93        new_image
94            .write_to(&mut cursor, image_data.format)
95            .expect("Failed to encode image");
96    }
97
98    return cursor.into_inner();
99}
100
101/// Sorts given pixels in their intervals and places them at the
102/// original positions on the new image.
103fn sort_pixels(
104    new_image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
105    pixel_positions: Vec<Vec<Vec<(u32, u32)>>>,
106    pixels_to_be_sorted: Vec<Vec<Vec<(Rgba<u8>, i32)>>>,
107) {
108    let mut i = 0;
109    for interval in pixels_to_be_sorted {
110        let mut j = 0;
111        for mut pixels in interval {
112            pixels.sort_by(|a, b| a.1.cmp(&b.1));
113
114            for (k, pixel) in pixels.iter().enumerate() {
115                new_image.put_pixel(
116                    pixel_positions[i][j][k].0,
117                    pixel_positions[i][j][k].1,
118                    pixel.0,
119                );
120            }
121
122            j += 1;
123        }
124
125        i += 1;
126    }
127}
128
129/// Calculates which pixels need to be sorted horizontally.
130fn get_horizontal_pixels_to_be_sorted(
131    image: &DynamicImage,
132    new_image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
133    avg_brightness: usize,
134) -> (Vec<Vec<Vec<(u32, u32)>>>, Vec<Vec<Vec<(Rgba<u8>, i32)>>>) {
135    let mut pixel_positions: Vec<Vec<Vec<(u32, u32)>>> = Vec::new();
136    let mut pixels_to_be_sorted: Vec<Vec<Vec<(Rgba<u8>, i32)>>> = Vec::new();
137
138    let mut current_row = 0;
139    let mut interval = 0;
140
141    for (i, pixel) in image.pixels().enumerate() {
142        let row_check = i / image.width() as usize;
143        if row_check > current_row {
144            current_row += 1;
145            interval = 0;
146        }
147
148        if pixel_positions.len() <= current_row {
149            pixel_positions.push(Vec::new());
150            pixels_to_be_sorted.push(Vec::new());
151        }
152
153        if pixel_positions[current_row].len() <= interval {
154            pixel_positions[current_row].push(Vec::new());
155            pixels_to_be_sorted[current_row].push(Vec::new());
156        }
157
158        let grayscale = pixel_to_grayscale_value(pixel);
159
160        if grayscale > avg_brightness as i32 {
161            pixel_positions[current_row][interval].push((pixel.0, pixel.1));
162
163            pixels_to_be_sorted[current_row][interval].push((pixel.2, grayscale));
164        } else {
165            new_image.put_pixel(pixel.0, pixel.1, pixel.2);
166            interval += 1;
167        }
168    }
169
170    (pixel_positions, pixels_to_be_sorted)
171}
172
173/// Calculates which pixels need to be sorted vertically.
174fn get_vertical_pixels_to_be_sorted(
175    image: &DynamicImage,
176    new_image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
177    avg_brightness: usize,
178) -> (Vec<Vec<Vec<(u32, u32)>>>, Vec<Vec<Vec<(Rgba<u8>, i32)>>>) {
179    let mut pixel_positions: Vec<Vec<Vec<(u32, u32)>>> = Vec::new();
180    let mut pixels_to_be_sorted: Vec<Vec<Vec<(Rgba<u8>, i32)>>> = Vec::new();
181
182    let width = image.width();
183    let height = image.height();
184
185    for x in 0..width {
186        let mut column_positions = Vec::new();
187        let mut column_pixels = Vec::new();
188
189        let mut interval = 0;
190        column_positions.push(Vec::new());
191        column_pixels.push(Vec::new());
192
193        for y in 0..height {
194            let pixel = image.get_pixel(x, y);
195            let grayscale = pixel_to_grayscale_value((x, y, pixel));
196
197            if grayscale > avg_brightness as i32 {
198                column_positions[interval].push((x, y));
199                column_pixels[interval].push((pixel, grayscale));
200            } else {
201                new_image.put_pixel(x, y, pixel);
202
203                if !column_positions[interval].is_empty() {
204                    interval += 1;
205                    column_positions.push(Vec::new());
206                    column_pixels.push(Vec::new());
207                }
208            }
209        }
210        pixel_positions.push(column_positions);
211        pixels_to_be_sorted.push(column_pixels);
212    }
213
214    (pixel_positions, pixels_to_be_sorted)
215}
216
217/// Prints the help text for this effect.
218#[cfg(not(target_arch = "wasm32"))]
219fn print_help() {
220    println!(
221        r#"
222Pixel Sorting Effect
223Sorts all pixels in an image above the image's average brightness by their
224brightness value.
225
226USAGE:
227  effectengine-cli pixel-sort <INPUT_PATH> <OUTPUT_PATH> [DIRECTION]
228
229ARGUMENTS:
230  <INPUT_PATH>     The path to an input image that should be processed.
231  <OUTPUT_PATH>    The path where the resulting image should be saved.
232                   Needs to include the filename.
233  [DIRECTION]      Optional. The direction the pixels should be sorted in.
234                   Valid options are "horizontal", "vertical" or "both".
235                   (Default: "horizontal")
236  "#
237    );
238}