photon_rs/
effects.rs

1//! Special effects.
2
3use crate::helpers;
4use crate::iter::ImageIterator;
5use crate::{PhotonImage, Rgb};
6use image::Pixel;
7use image::Rgba;
8use image::{GenericImage, GenericImageView};
9use imageproc::drawing::draw_filled_rect_mut;
10use imageproc::rect::Rect;
11use perlin2d::PerlinNoise2D;
12use std::collections::HashMap;
13use std::f64;
14
15#[cfg(feature = "enable_wasm")]
16use wasm_bindgen::prelude::*;
17
18/// Adds an offset to the image by a certain number of pixels.
19///
20/// This creates an RGB shift effect.
21///
22/// # Arguments
23/// * `img` - A PhotonImage that contains a view into the image.
24/// * `channel_index`: The index of the channel to increment. 0 for red, 1 for green and 2 for blue.
25/// * `offset` - The offset is added to the pixels in the image.
26/// # Example
27///
28/// ```no_run
29/// // For example, to offset pixels by 30 pixels on the red channel:
30/// use photon_rs::effects::offset;
31/// use photon_rs::native::open_image;
32///
33/// let mut img = open_image("img.jpg").expect("File should open");
34/// offset(&mut img, 0_usize, 30_u32);
35/// ```
36#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
37pub fn offset(photon_image: &mut PhotonImage, channel_index: usize, offset: u32) {
38    if channel_index > 2 {
39        panic!("Invalid channel index passed. Channel1 must be equal to 0, 1, or 2.");
40    }
41
42    let mut img = helpers::dyn_image_from_raw(photon_image);
43    let (width, height) = img.dimensions();
44
45    for x in 0..width - 10 {
46        for y in 0..height - 10 {
47            let px = img.get_pixel(x, y);
48
49            if x + offset < width - 1 && y + offset < height - 1 {
50                let offset_px = img.get_pixel(x + offset, y + offset);
51                let offset_px_channels = offset_px.channels();
52
53                let px_channels = px.channels();
54
55                let px = match channel_index {
56                    0 => image::Rgba([
57                        offset_px_channels[0],
58                        px_channels[1],
59                        px_channels[2],
60                        255,
61                    ]),
62                    1 => image::Rgba([
63                        px_channels[0],
64                        offset_px_channels[1],
65                        px_channels[2],
66                        255,
67                    ]),
68                    2 => image::Rgba([
69                        px_channels[0],
70                        px_channels[1],
71                        offset_px_channels[2],
72                        255,
73                    ]),
74                    _ => image::Rgba([
75                        px_channels[0],
76                        px_channels[1],
77                        offset_px_channels[2],
78                        255,
79                    ]),
80                };
81                img.put_pixel(x, y, px);
82            }
83        }
84    }
85    let raw_pixels = img.into_bytes();
86    photon_image.raw_pixels = raw_pixels;
87}
88
89/// Adds an offset to the red channel by a certain number of pixels.
90///
91/// # Arguments
92/// * `img` - A PhotonImage that contains a view into the image.
93/// * `offset` - The offset you want to move the red channel by.
94/// # Example
95///
96/// ```no_run
97/// // For example, to add an offset to the red channel by 30 pixels.
98/// use photon_rs::effects::offset_red;
99/// use photon_rs::native::open_image;
100///
101/// let mut img = open_image("img.jpg").expect("File should open");
102/// offset_red(&mut img, 30_u32);
103/// ```
104#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
105pub fn offset_red(img: &mut PhotonImage, offset_amt: u32) {
106    offset(img, 0, offset_amt)
107}
108
109/// Adds an offset to the green channel by a certain number of pixels.
110///
111/// # Arguments
112/// * `img` - A PhotonImage that contains a view into the image.
113/// * `offset` - The offset you want to move the green channel by.
114/// # Example
115///
116/// ```no_run
117/// // For example, to add an offset to the green channel by 30 pixels.
118/// use photon_rs::effects::offset_green;
119/// use photon_rs::native::open_image;
120///
121/// let mut img = open_image("img.jpg").expect("File should open");
122/// offset_green(&mut img, 30_u32);
123/// ```
124#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
125pub fn offset_green(img: &mut PhotonImage, offset_amt: u32) {
126    offset(img, 1, offset_amt)
127}
128
129/// Adds an offset to the blue channel by a certain number of pixels.
130///
131/// # Arguments
132/// * `img` - A PhotonImage that contains a view into the image.
133/// * `offset_amt` - The offset you want to move the blue channel by.
134/// # Example
135/// // For example, to add an offset to the green channel by 40 pixels.
136///
137/// ```no_run
138/// use photon_rs::effects::offset_blue;
139/// use photon_rs::native::open_image;
140///
141/// let mut img = open_image("img.jpg").expect("File should open");
142/// offset_blue(&mut img, 40_u32);
143/// ```
144#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
145pub fn offset_blue(img: &mut PhotonImage, offset_amt: u32) {
146    offset(img, 2, offset_amt)
147}
148
149/// Adds multiple offsets to the image by a certain number of pixels (on two channels).
150///
151/// # Arguments
152/// * `img` - A PhotonImage that contains a view into the image.
153/// * `offset` - The offset is added to the pixels in the image.
154/// # Example
155///
156/// ```no_run
157/// // For example, to add a 30-pixel offset to both the red and blue channels:
158/// use photon_rs::effects::multiple_offsets;
159/// use photon_rs::native::open_image;
160///
161/// let mut img = open_image("img.jpg").expect("File should open");
162/// multiple_offsets(&mut img, 30_u32, 0_usize, 2_usize);
163/// ```
164#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
165pub fn multiple_offsets(
166    photon_image: &mut PhotonImage,
167    offset: u32,
168    channel_index: usize,
169    channel_index2: usize,
170) {
171    if channel_index > 2 {
172        panic!("Invalid channel index passed. Channel1 must be equal to 0, 1, or 2.");
173    }
174    if channel_index2 > 2 {
175        panic!("Invalid channel index passed. Channel2 must be equal to 0, 1, or 2.");
176    }
177    let mut img = helpers::dyn_image_from_raw(photon_image);
178    let (width, height) = img.dimensions();
179
180    for (x, y) in ImageIterator::new(width, height) {
181        let mut px = img.get_pixel(x, y);
182
183        if x + offset < width - 1 && y + offset < height - 1 {
184            let offset_px = img.get_pixel(x + offset, y);
185
186            px[channel_index] = offset_px[channel_index];
187        }
188
189        if x as i32 - offset as i32 > 0 && y as i32 - offset as i32 > 0 {
190            let offset_px2 = img.get_pixel(x - offset, y);
191
192            px[channel_index2] = offset_px2[channel_index2];
193        }
194
195        img.put_pixel(x, y, px);
196    }
197    let raw_pixels = img.into_bytes();
198    photon_image.raw_pixels = raw_pixels;
199}
200
201/// Halftoning effect.
202///
203/// # Arguments
204/// * `img` - A PhotonImage that contains a view into the image.
205/// # Example
206///
207/// ```no_run
208/// // For example:
209/// use photon_rs::effects::halftone;
210/// use photon_rs::native::open_image;
211///
212/// let mut img = open_image("img.jpg").expect("File should open");
213/// halftone(&mut img);
214/// ```
215#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
216pub fn halftone(photon_image: &mut PhotonImage) {
217    let mut img = helpers::dyn_image_from_raw(photon_image);
218    let (width, height) = img.dimensions();
219
220    for x in (0..width - 4).step_by(2_usize) {
221        for y in (0..height - 4).step_by(2_usize) {
222            let mut px1 = img.get_pixel(x, y);
223            let mut px2 = img.get_pixel(x, y + 1);
224            let mut px3 = img.get_pixel(x + 1, y);
225            let mut px4 = img.get_pixel(x + 1, y + 1);
226
227            let gray1 = (px1[0] as f64 * 0.299)
228                + (px1[1] as f64 * 0.587)
229                + (px1[2] as f64 * 0.114);
230            let gray2 = (px2[0] as f64 * 0.299)
231                + (px2[1] as f64 * 0.587)
232                + (px2[2] as f64 * 0.114);
233            let gray3 = (px3[0] as f64 * 0.299)
234                + (px3[1] as f64 * 0.587)
235                + (px3[2] as f64 * 0.114);
236            let gray4 = (px4[0] as f64 * 0.299)
237                + (px4[1] as f64 * 0.587)
238                + (px4[2] as f64 * 0.114);
239
240            let sat = (gray1 + gray2 + gray3 + gray4) / 4.0;
241
242            if sat > 200.0 {
243                px1[0] = 255;
244                px1[1] = 255;
245                px1[2] = 255;
246
247                px2[0] = 255;
248                px2[1] = 255;
249                px2[2] = 255;
250
251                px3[0] = 255;
252                px3[1] = 255;
253                px3[2] = 255;
254
255                px4[0] = 255;
256                px4[1] = 255;
257                px4[2] = 255;
258            } else if sat > 159.0 {
259                px1[0] = 255;
260                px1[1] = 255;
261                px1[2] = 255;
262
263                px2[0] = 0;
264                px2[1] = 0;
265                px2[2] = 0;
266
267                px3[0] = 255;
268                px3[1] = 255;
269                px3[2] = 255;
270
271                px4[0] = 255;
272                px4[1] = 255;
273                px4[2] = 255;
274            } else if sat > 95.0 {
275                px1[0] = 255;
276                px1[1] = 255;
277                px1[2] = 255;
278
279                px2[0] = 0;
280                px2[1] = 0;
281                px2[2] = 0;
282
283                px3[0] = 0;
284                px3[1] = 0;
285                px3[2] = 0;
286
287                px4[0] = 255;
288                px4[1] = 255;
289                px4[2] = 255;
290            } else if sat > 32.0 {
291                px1[0] = 0;
292                px1[1] = 0;
293                px1[2] = 0;
294
295                px2[0] = 255;
296                px2[1] = 255;
297                px2[2] = 255;
298
299                px3[0] = 0;
300                px3[1] = 0;
301                px3[2] = 0;
302
303                px4[0] = 0;
304                px4[1] = 0;
305                px4[2] = 0;
306            } else {
307                px1[0] = 0;
308                px1[1] = 0;
309                px1[2] = 0;
310
311                px2[0] = 0;
312                px2[1] = 0;
313                px2[2] = 0;
314
315                px3[0] = 0;
316                px3[1] = 0;
317                px3[2] = 0;
318
319                px4[0] = 0;
320                px4[1] = 0;
321                px4[2] = 0;
322            }
323
324            img.put_pixel(x, y, px1);
325            // img.put_pixel(x, y + 1, px2);
326        }
327    }
328    let raw_pixels = img.into_bytes();
329    photon_image.raw_pixels = raw_pixels;
330}
331
332/// Reduces an image to the primary colours.
333///
334/// # Arguments
335/// * `img` - A PhotonImage that contains a view into the image.
336/// # Example
337///
338/// ```no_run
339/// // For example, to add a primary colour effect to an image of type `DynamicImage`:
340/// use photon_rs::effects::primary;
341/// use photon_rs::native::open_image;
342///
343/// let mut img = open_image("img.jpg").expect("File should open");
344/// primary(&mut img);
345/// ```
346#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
347pub fn primary(img: &mut PhotonImage) {
348    let end = img.raw_pixels.len() - 4;
349
350    for i in (0..end).step_by(4) {
351        let mut r_val = img.raw_pixels[0];
352        let mut g_val = img.raw_pixels[1];
353        let mut b_val = img.raw_pixels[2];
354
355        if r_val > 128 {
356            r_val = 255;
357        } else {
358            r_val = 0;
359        }
360
361        if g_val > 128 {
362            g_val = 255;
363        } else {
364            g_val = 0;
365        }
366
367        if b_val > 128 {
368            g_val = 255;
369        } else {
370            b_val = 0;
371        }
372
373        img.raw_pixels[i] = r_val;
374        img.raw_pixels[i + 1] = g_val;
375        img.raw_pixels[i + 2] = b_val;
376    }
377}
378
379/// Colorizes the green channels of the image.
380///
381/// # Arguments
382/// * `img` - A PhotonImage that contains a view into the image.
383/// # Example
384///
385/// ```no_run
386/// // For example, to colorize an image of type `PhotonImage`:
387/// use photon_rs::effects::colorize;
388/// use photon_rs::native::open_image;
389///
390/// let mut img = open_image("img.jpg").expect("File should open");
391/// colorize(&mut img);
392/// ```
393#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
394pub fn colorize(photon_image: &mut PhotonImage) {
395    let mut img = helpers::dyn_image_from_raw(photon_image);
396    let threshold = 220;
397
398    for (x, y) in ImageIterator::with_dimension(&img.dimensions()) {
399        let mut px = img.get_pixel(x, y);
400        let channels = px.channels();
401        let px_as_rgb = Rgb {
402            r: channels[0],
403            g: channels[1],
404            b: channels[2],
405        };
406
407        let baseline_color = Rgb {
408            r: 0,
409            g: 255,
410            b: 255,
411        };
412
413        let square_distance = crate::helpers::square_distance(baseline_color, px_as_rgb);
414
415        let mut r = channels[0] as f32;
416        let mut g = channels[1] as f32;
417        let mut b = channels[2] as f32;
418
419        if square_distance < i32::pow(threshold, 2) {
420            r *= 0.5;
421            g *= 1.25;
422            b *= 0.5;
423        }
424
425        px = image::Rgba([r as u8, g as u8, b as u8, 255]);
426        img.put_pixel(x, y, px);
427    }
428    let raw_pixels = img.into_bytes();
429    photon_image.raw_pixels = raw_pixels;
430}
431
432// #[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
433// pub fn inc_luminosity(mut photon_image: PhotonImage) -> PhotonImage {
434//     let mut img = helpers::dyn_image_from_raw(photon_image);
435//     let (width, height) = img.dimensions();
436//     let mut min_intensity = 255;
437//     let mut max_intensity = 0;
438
439//     // find the max and min intensities in the image
440//     for x in 0..width {
441//         for y in 0..height {
442//             let px = img.get_pixel(x, y);
443//             let intensity = (px.data[0] as u32 + px.data[1] as u32 + px.data[2] as u32) / 3;
444//             if intensity > 0{
445//                 min_intensity = cmp::min(min_intensity, intensity);
446//                 max_intensity = cmp::max(max_intensity, intensity);
447//             }
448
449//         }
450//     }
451
452//     for x in 0..width {
453//         for y in 0..height {
454//             let mut px = img.get_pixel(x, y);
455//             // let px_as_rgb = Rgb{r: px.data[0], g: px.data[1], b: px.data[2]};
456
457//             let mut r = px.data[0] as f32;
458//             let mut g = px.data[1] as f32;
459//             let mut b = px.data[2] as f32;
460
461//             let lum = (r + g + b) / 3.0;
462
463//             let new_lum = 255.0 * (lum - min_intensity as f32) / (max_intensity / min_intensity) as f32;
464
465//             r = r * new_lum / lum;
466//             g = g * new_lum / lum;
467//             b = b * new_lum / lum;
468
469//             px.data[0] = r as u8;
470//             px.data[1] = g as u8;
471//             px.data[2] = b as u8;
472
473//             img.put_pixel(x, y, px);
474//         }
475//     }
476//     let mut raw_pixels = img.raw_pixels();
477//     photon_image.raw_pixels = raw_pixels;
478//     photon_image
479// }
480
481/// Applies a solarizing effect to an image.
482///
483/// # Arguments
484/// * `img` - A PhotonImage that contains a view into the image.
485/// # Example
486///
487/// ```no_run
488/// // For example, to colorize an image of type `PhotonImage`:
489/// use photon_rs::effects::solarize;
490/// use photon_rs::native::open_image;
491///
492/// let mut img = open_image("img.jpg").expect("File should open");
493/// solarize(&mut img);
494/// ```
495#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
496pub fn solarize(photon_image: &mut PhotonImage) {
497    let end = photon_image.get_raw_pixels().len();
498
499    for i in (0..end).step_by(4) {
500        let r_val = photon_image.raw_pixels[i];
501
502        if 200 - r_val as i32 > 0 {
503            photon_image.raw_pixels[i] = 200 - r_val;
504        }
505    }
506}
507
508/// Applies a solarizing effect to an image and returns the resulting PhotonImage.
509///
510/// # Arguments
511/// * `img` - A PhotonImage that contains a view into the image.
512/// # Example
513///
514/// ```no_run
515/// // For example, to solarize "retimg" an image of type `PhotonImage`:
516/// use photon_rs::effects::solarize_retimg;
517/// use photon_rs::native::open_image;
518/// use photon_rs::PhotonImage;
519///
520/// let img = open_image("img.jpg").expect("File should open");
521/// let result: PhotonImage = solarize_retimg(&img);
522/// ```
523#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
524pub fn solarize_retimg(photon_image: &PhotonImage) -> PhotonImage {
525    let mut img = helpers::dyn_image_from_raw(photon_image);
526
527    for (x, y) in ImageIterator::with_dimension(&img.dimensions()) {
528        let mut px = img.get_pixel(x, y);
529        let channels = px.channels();
530        if 200_i32 - channels[0] as i32 > 0 {
531            let new_r_val = 200 - channels[0];
532            px = image::Rgba([new_r_val, channels[1], channels[2], channels[3]]);
533        }
534        img.put_pixel(x, y, px);
535    }
536
537    let (width, height) = img.dimensions();
538
539    PhotonImage {
540        raw_pixels: img.into_bytes(),
541        width,
542        height,
543    }
544}
545
546/// Adjust the brightness of an image by a factor.
547///
548/// # Arguments
549/// * `img` - A PhotonImage that contains a view into the image.
550/// * `brightness` - A u8 to add or subtract to the brightness. To increase
551/// the brightness, pass a positive number (up to 255). To decrease the brightness,
552/// pass a negative number instead.
553/// # Example
554///
555/// ```no_run
556/// use photon_rs::effects::adjust_brightness;
557/// use photon_rs::native::open_image;
558///
559/// let mut img = open_image("img.jpg").expect("File should open");
560/// adjust_brightness(&mut img, 10_i16);
561/// ```
562#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
563pub fn adjust_brightness(photon_image: &mut PhotonImage, brightness: i16) {
564    if brightness > 0 {
565        inc_brightness(photon_image, brightness as u8)
566    } else {
567        dec_brightness(photon_image, brightness.unsigned_abs() as u8)
568    }
569}
570
571/// Increase the brightness of an image by a constant.
572///
573/// # Arguments
574/// * `img` - A PhotonImage that contains a view into the image.
575/// * `brightness` - A u8 to add to the brightness.
576/// # Example
577///
578/// ```no_run
579/// use photon_rs::effects::inc_brightness;
580/// use photon_rs::native::open_image;
581///
582/// let mut img = open_image("img.jpg").expect("File should open");
583/// inc_brightness(&mut img, 10_u8);
584/// ```
585#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
586pub fn inc_brightness(photon_image: &mut PhotonImage, brightness: u8) {
587    let end = photon_image.get_raw_pixels().len() - 4;
588
589    for i in (0..end).step_by(4) {
590        let r_val = photon_image.raw_pixels[i];
591        let g_val = photon_image.raw_pixels[i + 1];
592        let b_val = photon_image.raw_pixels[i + 2];
593
594        if r_val <= 255 - brightness {
595            photon_image.raw_pixels[i] += brightness;
596        } else {
597            photon_image.raw_pixels[i] = 255;
598        }
599        if g_val <= 255 - brightness {
600            photon_image.raw_pixels[i + 1] += brightness;
601        } else {
602            photon_image.raw_pixels[i + 1] = 255
603        }
604
605        if b_val <= 255 - brightness {
606            photon_image.raw_pixels[i + 2] += brightness;
607        } else {
608            photon_image.raw_pixels[i + 2] = 255
609        }
610    }
611}
612
613/// Decrease the brightness of an image by a constant.
614///
615/// # Arguments
616/// * `img` - A PhotonImage that contains a view into the image.
617/// * `brightness` - A u8 to subtract from the brightness. It should be a positive number,
618/// and this value will then be subtracted from the brightness.
619/// # Example
620///
621/// ```no_run
622/// use photon_rs::effects::dec_brightness;
623/// use photon_rs::native::open_image;
624///
625/// let mut img = open_image("img.jpg").expect("File should open");
626/// dec_brightness(&mut img, 10_u8);
627/// ```
628#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
629pub fn dec_brightness(photon_image: &mut PhotonImage, brightness: u8) {
630    // println!("{} is brightness", brightness);
631
632    let end = photon_image.get_raw_pixels().len() - 4;
633
634    for i in (0..end).step_by(4) {
635        photon_image.raw_pixels[i] =
636            photon_image.raw_pixels[i].saturating_sub(brightness);
637        photon_image.raw_pixels[i + 1] =
638            photon_image.raw_pixels[i + 1].saturating_sub(brightness);
639        photon_image.raw_pixels[i + 2] =
640            photon_image.raw_pixels[i + 2].saturating_sub(brightness);
641    }
642}
643
644/// Adjust the contrast of an image by a factor.
645///
646/// # Arguments
647/// * `photon_image` - A PhotonImage that contains a view into the image.
648/// * `contrast` - An f32 factor used to adjust contrast. Between [-255.0, 255.0]. The algorithm will
649/// clamp results if passed factor is out of range.
650/// # Example
651///
652/// ```no_run
653/// use photon_rs::effects::adjust_contrast;
654/// use photon_rs::native::open_image;
655///
656/// let mut img = open_image("img.jpg").expect("File should open");
657/// adjust_contrast(&mut img, 30_f32);
658/// ```
659#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
660pub fn adjust_contrast(photon_image: &mut PhotonImage, contrast: f32) {
661    let mut img = helpers::dyn_image_from_raw(photon_image);
662
663    let clamped_contrast = contrast.clamp(-255.0, 255.0);
664
665    // Some references:
666    // https://math.stackexchange.com/questions/906240/algorithms-to-increase-or-decrease-the-contrast-of-an-image
667    // https://www.dfstudios.co.uk/articles/programming/image-programming-algorithms/image-processing-algorithms-part-5-contrast-adjustment/
668    let factor =
669        (259.0 * (clamped_contrast + 255.0)) / (255.0 * (259.0 - clamped_contrast));
670    let mut lookup_table: Vec<u8> = vec![0; 256];
671    let offset = -128.0 * factor + 128.0;
672
673    for (i, table) in lookup_table.iter_mut().enumerate().take(256_usize) {
674        let new_val = i as f32 * factor + offset;
675        *table = new_val.clamp(0.0, 255.0) as u8;
676    }
677
678    for (x, y) in ImageIterator::with_dimension(&img.dimensions()) {
679        let mut px = img.get_pixel(x, y);
680        let channels = px.channels();
681
682        px = image::Rgba([
683            lookup_table[channels[0] as usize],
684            lookup_table[channels[1] as usize],
685            lookup_table[channels[2] as usize],
686            255,
687        ]);
688        img.put_pixel(x, y, px);
689    }
690
691    photon_image.raw_pixels = img.into_bytes();
692}
693
694/// Tint an image by adding an offset to averaged RGB channel values.
695///
696/// # Arguments
697/// * `img` - A PhotonImage that contains a view into the image.
698/// * `r_offset` - The amount the R channel should be incremented by.
699/// * `g_offset` - The amount the G channel should be incremented by.
700/// * `b_offset` - The amount the B channel should be incremented by.
701/// # Example
702///
703/// ```no_run
704/// // For example, to tint an image of type `PhotonImage`:
705/// use photon_rs::effects::tint;
706/// use photon_rs::native::open_image;
707///
708/// let mut img = open_image("img.jpg").expect("File should open");
709/// tint(&mut img, 10_u32, 20_u32, 15_u32);
710/// ```
711///
712#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
713pub fn tint(
714    photon_image: &mut PhotonImage,
715    r_offset: u32,
716    g_offset: u32,
717    b_offset: u32,
718) {
719    let mut img = helpers::dyn_image_from_raw(photon_image);
720
721    for (x, y) in ImageIterator::with_dimension(&img.dimensions()) {
722        let mut px = img.get_pixel(x, y);
723        let channels = px.channels();
724        let (r_val, g_val, b_val) =
725            (channels[0] as u32, channels[1] as u32, channels[2] as u32);
726
727        let new_r_val = if r_val + r_offset < 255 {
728            r_val as u8 + r_offset as u8
729        } else {
730            255
731        };
732        let new_g_val = if g_val + g_offset < 255 {
733            g_val as u8 + g_offset as u8
734        } else {
735            255
736        };
737        let new_b_val = if b_val + b_offset < 255 {
738            b_val as u8 + b_offset as u8
739        } else {
740            255
741        };
742
743        px = image::Rgba([new_r_val, new_g_val, new_b_val, 255]);
744
745        img.put_pixel(x, y, px);
746    }
747
748    let raw_pixels = img.into_bytes();
749    photon_image.raw_pixels = raw_pixels;
750}
751
752fn draw_horizontal_strips(photon_image: &mut PhotonImage, num_strips: u8, color: Rgb) {
753    let mut img = helpers::dyn_image_from_raw(photon_image);
754    let (width, height) = img.dimensions();
755
756    let total_strips = (num_strips * 2) - 1;
757    let height_strip = height / total_strips as u32;
758    let mut y_pos: u32 = 0;
759    for i in 1..num_strips {
760        draw_filled_rect_mut(
761            &mut img,
762            Rect::at(0, (y_pos + height_strip) as i32).of_size(width, height_strip),
763            Rgba([color.r, color.g, color.b, 255u8]),
764        );
765        y_pos = i as u32 * (height_strip * 2);
766    }
767
768    let raw_pixels = img.into_bytes();
769    photon_image.raw_pixels = raw_pixels;
770}
771
772/// Horizontal strips. Divide an image into a series of equal-height strips, for an artistic effect.
773///
774/// # Arguments
775/// * `img` - A PhotonImage that contains a view into the image.
776/// * `num_strips` - The number of strips
777/// # Example
778///
779/// ```no_run
780/// // For example, to draw horizontal strips on a `PhotonImage`:
781/// use photon_rs::effects::horizontal_strips;
782/// use photon_rs::native::open_image;
783///
784/// let mut img = open_image("img.jpg").expect("File should open");
785/// horizontal_strips(&mut img, 8u8);
786/// ```
787///
788#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
789pub fn horizontal_strips(photon_image: &mut PhotonImage, num_strips: u8) {
790    let color = Rgb {
791        r: 255,
792        g: 255,
793        b: 255,
794    };
795    draw_horizontal_strips(photon_image, num_strips, color)
796}
797
798/// Horizontal strips. Divide an image into a series of equal-width strips, for an artistic effect. Sepcify a color as well.
799///
800/// # Arguments
801/// * `img` - A PhotonImage that contains a view into the image.
802/// * `num_strips` - The numbder of strips
803/// * `color` - Color of strips.
804/// # Example
805///
806/// ```no_run
807/// // For example, to draw blue horizontal strips on a `PhotonImage`:
808/// use photon_rs::effects::color_horizontal_strips;
809/// use photon_rs::native::open_image;
810/// use photon_rs::Rgb;
811///
812/// let color = Rgb::new(255u8, 0u8, 0u8);
813/// let mut img = open_image("img.jpg").expect("File should open");
814/// color_horizontal_strips(&mut img, 8u8, color);
815/// ```
816///
817#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
818pub fn color_horizontal_strips(
819    photon_image: &mut PhotonImage,
820    num_strips: u8,
821    color: Rgb,
822) {
823    draw_horizontal_strips(photon_image, num_strips, color)
824}
825
826fn draw_vertical_strips(photon_image: &mut PhotonImage, num_strips: u8, color: Rgb) {
827    let mut img = helpers::dyn_image_from_raw(photon_image);
828    let (width, height) = img.dimensions();
829
830    let total_strips = (num_strips * 2) - 1;
831    let width_strip = width / total_strips as u32;
832    let mut x_pos: u32 = 0;
833    for i in 1..num_strips {
834        draw_filled_rect_mut(
835            &mut img,
836            Rect::at((x_pos + width_strip) as i32, 0).of_size(width_strip, height),
837            Rgba([color.r, color.g, color.b, 255u8]),
838        );
839        x_pos = i as u32 * (width_strip * 2);
840    }
841
842    let raw_pixels = img.into_bytes();
843    photon_image.raw_pixels = raw_pixels;
844}
845
846/// Vertical strips. Divide an image into a series of equal-width strips, for an artistic effect.
847///
848/// # Arguments
849/// * `img` - A PhotonImage that contains a view into the image.
850/// * `num_strips` - The numbder of strips
851/// # Example
852///
853/// ```no_run
854/// // For example, to draw vertical strips on a `PhotonImage`:
855/// use photon_rs::effects::vertical_strips;
856/// use photon_rs::native::open_image;
857///
858/// let mut img = open_image("img.jpg").expect("File should open");
859/// vertical_strips(&mut img, 8u8);
860/// ```
861///
862#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
863pub fn vertical_strips(photon_image: &mut PhotonImage, num_strips: u8) {
864    let color = Rgb {
865        r: 255,
866        g: 255,
867        b: 255,
868    };
869    draw_vertical_strips(photon_image, num_strips, color)
870}
871
872/// Vertical strips. Divide an image into a series of equal-width strips, for an artistic effect. Sepcify a color as well.
873///
874/// # Arguments
875/// * `img` - A PhotonImage that contains a view into the image.
876/// * `num_strips` - The numbder of strips
877/// * `color` - Color of strips.
878/// # Example
879///
880/// ```no_run
881/// // For example, to draw red vertical strips on a `PhotonImage`:
882/// use photon_rs::effects::color_vertical_strips;
883/// use photon_rs::native::open_image;
884/// use photon_rs::Rgb;
885///
886/// let color = Rgb::new(255u8, 0u8, 0u8);
887/// let mut img = open_image("img.jpg").expect("File should open");
888/// color_vertical_strips(&mut img, 8u8, color);
889/// ```
890///
891#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
892pub fn color_vertical_strips(
893    photon_image: &mut PhotonImage,
894    num_strips: u8,
895    color: Rgb,
896) {
897    draw_vertical_strips(photon_image, num_strips, color)
898}
899
900struct Intensity {
901    val: i32,
902    r: i32,
903    g: i32,
904    b: i32,
905}
906/// Turn an image into an oil painting
907///
908/// # Arguments
909/// * `img` - A PhotonImage that contains a view into the image.
910/// * `radius` - Radius of each paint particle
911/// * `intesnity` - How artsy an Image should be
912/// # Example
913///
914/// ```no_run
915/// // For example, to oil an image of type `PhotonImage`:
916/// use photon_rs::effects::oil;
917/// use photon_rs::native::open_image;
918///
919/// let mut img = open_image("img.jpg").expect("File should open");
920/// oil(&mut img, 4i32, 55.0);
921/// ```
922///
923#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
924pub fn oil(photon_image: &mut PhotonImage, radius: i32, intensity: f64) {
925    let img = helpers::dyn_image_from_raw(photon_image);
926    let (width, height) = img.dimensions();
927    let mut target = image::DynamicImage::new_rgba8(width, height);
928    let mut pixel_intensity_count: HashMap<usize, Intensity>;
929    let mut intensity_lut = vec![vec![0; width as usize]; height as usize];
930
931    for y in 0..height {
932        for x in 0..width {
933            let single_pix = img.get_pixel(x, y);
934            let current_val = single_pix.channels();
935            let avg = (current_val[0] as i32
936                + current_val[1] as i32
937                + current_val[2] as i32) as f64
938                / 3.0;
939            let val = (avg * intensity) / 255.0;
940            intensity_lut[y as usize][x as usize] = val.round() as usize;
941        }
942    }
943
944    for y in 0..height {
945        for x in 0..width {
946            pixel_intensity_count = HashMap::new();
947
948            for yy in -radius..=radius {
949                let yyy = (y as i32) + yy;
950                for xx in -radius..=radius {
951                    let xxx = (x as i32) + xx;
952                    if yyy > 0
953                        && yyy < (height as i32)
954                        && xxx > 0
955                        && xxx < (width as i32)
956                    {
957                        let idx_x = xxx as usize;
958                        let idx_y = yyy as usize;
959                        let intensity_val = intensity_lut[idx_y][idx_x];
960                        let single_pix = img.get_pixel(idx_x as u32, idx_y as u32);
961                        let pix = single_pix.channels();
962                        match pixel_intensity_count.get_mut(&(intensity_val)) {
963                            Some(val) => {
964                                val.val += 1;
965                                val.r += pix[0] as i32;
966                                val.g += pix[1] as i32;
967                                val.b += pix[2] as i32;
968                            }
969                            None => {
970                                pixel_intensity_count.insert(
971                                    intensity_val,
972                                    Intensity {
973                                        val: 1,
974                                        r: pix[0] as i32,
975                                        g: pix[1] as i32,
976                                        b: pix[2] as i32,
977                                    },
978                                );
979                            }
980                        }
981                    }
982                }
983            }
984
985            let mut map_vec: Vec<_> = pixel_intensity_count.iter().collect();
986            map_vec.sort_by(|a, b| (b.1.val - a.1.val).cmp(&0));
987
988            let cur_max = map_vec[0].1;
989            target.put_pixel(
990                x,
991                y,
992                Rgba::<u8>([
993                    (cur_max.r / cur_max.val) as u8,
994                    (cur_max.g / cur_max.val) as u8,
995                    (cur_max.b / cur_max.val) as u8,
996                    255,
997                ]),
998            )
999        }
1000    }
1001    let raw_pixels = target.into_bytes();
1002    photon_image.raw_pixels = raw_pixels;
1003}
1004/// Turn an image into an frosted glass see through
1005///
1006/// # Arguments
1007/// * `img` - A PhotonImage that contains a view into the image.
1008/// # Example
1009///
1010/// ```no_run
1011/// // For example, to turn an image of type `PhotonImage` into frosted glass see through:
1012/// use photon_rs::effects::frosted_glass;
1013/// use photon_rs::native::open_image;
1014///
1015/// let mut img = open_image("img.jpg").expect("File should open");
1016/// frosted_glass(&mut img);
1017/// ```
1018///
1019#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
1020pub fn frosted_glass(photon_image: &mut PhotonImage) {
1021    let img_orig_buf = photon_image.get_raw_pixels();
1022    let width = photon_image.get_width();
1023    let height = photon_image.get_height();
1024    let end = img_orig_buf.len();
1025
1026    let mut img_buf = Vec::<u8>::new();
1027    Vec::resize(&mut img_buf, end, 0_u8);
1028
1029    let perlin = PerlinNoise2D::new(2, 10.0, 10.0, 10.0, 1.0, (100.0, 100.0), 0.5, 101);
1030
1031    for pixel in (0..end).step_by(4) {
1032        let x = (pixel / 4) / width as usize;
1033        let y = (pixel / 4) % width as usize;
1034
1035        let res = [
1036            perlin.get_noise(x as f64, y as f64) - 0.5,
1037            (perlin.get_noise(100.0 + x as f64, y as f64) - 0.5) * 4.0,
1038        ];
1039
1040        let x_new = f64::clamp(f64::floor(x as f64 + res[0]), 0.0, height as f64 - 1.0);
1041        let x_new = x_new as usize;
1042        let y_new = f64::clamp(f64::floor(y as f64 + res[1]), 0.0, width as f64 - 1.0);
1043        let y_new = y_new as usize;
1044
1045        let pixel_new = (x_new * width as usize + y_new) * 4;
1046        if pixel_new > end {
1047            continue;
1048        }
1049        img_buf[pixel] = img_orig_buf[pixel_new];
1050        img_buf[pixel + 1] = img_orig_buf[pixel_new + 1];
1051        img_buf[pixel + 2] = img_orig_buf[pixel_new + 2];
1052        img_buf[pixel + 3] = img_orig_buf[pixel_new + 3];
1053    }
1054
1055    photon_image.raw_pixels = img_buf;
1056}
1057
1058/// Pixelize an image.
1059///
1060/// # Arguments
1061/// * `photon_image` - A PhotonImage that contains a view into the image.
1062/// * `pixel_size` - Targeted pixel size of generated image.
1063/// # Example
1064///
1065/// ```no_run
1066/// // For example, to turn an image of type `PhotonImage` into a pixelized image with 50 pixels blocks:
1067/// use photon_rs::effects::pixelize;
1068/// use photon_rs::native::open_image;
1069///
1070/// let mut img = open_image("img.jpg").expect("File should open");
1071/// pixelize(&mut img, 50);
1072/// ```
1073///
1074#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
1075pub fn pixelize(photon_image: &mut PhotonImage, pixel_size: i32) {
1076    let step_size = pixel_size.max(0) as usize;
1077    if step_size <= 1 {
1078        return;
1079    }
1080
1081    let buf = photon_image.raw_pixels.as_mut_slice();
1082    let width = photon_image.width as usize;
1083    let height = photon_image.height as usize;
1084
1085    for sy in (0..height).step_by(step_size) {
1086        let src_row_index = sy * width;
1087
1088        for sx in (0..width).step_by(step_size) {
1089            let src_index = 4 * (src_row_index + sx);
1090            let block_end_y = (sy + step_size).min(height);
1091            let block_end_x = (sx + step_size).min(width);
1092
1093            for dy in sy..block_end_y {
1094                let dst_row_index = dy * width;
1095
1096                for dx in sx..block_end_x {
1097                    let dst_index = 4 * (dst_row_index + dx);
1098                    buf[dst_index] = buf[src_index];
1099                    buf[dst_index + 1] = buf[src_index + 1];
1100                    buf[dst_index + 2] = buf[src_index + 2];
1101                    buf[dst_index + 3] = buf[src_index + 3];
1102                }
1103            }
1104        }
1105    }
1106}
1107
1108/// Normalizes an image by remapping its range of pixels values. Only RGB
1109/// channels are processed and each channel is stretched to \[0, 255\] range
1110/// independently. This process is also known as contrast stretching.
1111/// # Arguments
1112/// * `photon_image` - A PhotonImage that contains a view into the image.
1113/// # Example
1114///
1115/// ```no_run
1116/// // For example, to turn an image of type `PhotonImage` into a normalized image:
1117/// use photon_rs::effects::normalize;
1118/// use photon_rs::native::open_image;
1119///
1120/// let mut img = open_image("img.jpg").expect("File should open");
1121/// normalize(&mut img);
1122/// ```
1123///
1124#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
1125pub fn normalize(photon_image: &mut PhotonImage) {
1126    let buf = photon_image.raw_pixels.as_mut_slice();
1127    let buf_size = buf.len();
1128
1129    let mut min_rgb = Rgb::new(255, 255, 255);
1130    let mut max_rgb = Rgb::new(0, 0, 0);
1131
1132    for i in (0..buf_size).step_by(4) {
1133        let r = buf[i];
1134        let g = buf[i + 1];
1135        let b = buf[i + 2];
1136
1137        min_rgb.r = min_rgb.r.min(r);
1138        min_rgb.g = min_rgb.g.min(g);
1139        min_rgb.b = min_rgb.b.min(b);
1140
1141        max_rgb.r = max_rgb.r.max(r);
1142        max_rgb.g = max_rgb.g.max(g);
1143        max_rgb.b = max_rgb.b.max(b);
1144    }
1145
1146    let min_r = min_rgb.r as i32;
1147    let min_g = min_rgb.g as i32;
1148    let min_b = min_rgb.b as i32;
1149    let delta_r = (max_rgb.r as i32) - min_r;
1150    let delta_g = (max_rgb.g as i32) - min_g;
1151    let delta_b = (max_rgb.b as i32) - min_b;
1152
1153    for i in (0..buf_size).step_by(4) {
1154        let r = buf[i] as i32;
1155        let g = buf[i + 1] as i32;
1156        let b = buf[i + 2] as i32;
1157
1158        if delta_r > 0 {
1159            buf[i] = (((r - min_r) * 255) / delta_r) as u8;
1160        }
1161
1162        if delta_b > 0 {
1163            buf[i + 1] = (((g - min_g) * 255) / delta_g) as u8;
1164        }
1165
1166        if delta_b > 0 {
1167            buf[i + 2] = (((b - min_b) * 255) / delta_b) as u8;
1168        }
1169    }
1170}
1171
1172/// Applies Floyd-Steinberg dithering to an image.
1173/// Only RGB channels are processed, alpha remains unchanged.
1174/// # Arguments
1175/// * `photon_image` - A PhotonImage that contains a view into the image.
1176/// * `depth` - bits per channel. Clamped between 1 and 8.
1177/// # Example
1178///
1179/// ```no_run
1180/// // For example, to turn an image of type `PhotonImage` into a dithered image:
1181/// use photon_rs::effects::dither;
1182/// use photon_rs::native::open_image;
1183///
1184/// let mut img = open_image("img.jpg").expect("File should open");
1185/// let depth = 1;
1186/// dither(&mut img, depth);
1187/// ```
1188///
1189#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
1190pub fn dither(photon_image: &mut PhotonImage, depth: u32) {
1191    let width = photon_image.get_width() as usize;
1192    let height = photon_image.get_height() as usize;
1193    let buf = photon_image.raw_pixels.as_mut_slice();
1194    let channels = 4;
1195    let chan_stride = 1;
1196    let col_stride = chan_stride * channels;
1197    let row_stride = col_stride * width;
1198
1199    // Depth basically specifies the number of colours, e.g. when depth is 1,
1200    // than means monochrome values in each channel:
1201    // Number of colours = 2 ^ depth = 2 ^ 1 = 2
1202    // In order to resample pixel, the original value must be downscaled to [0..colours] range
1203    // (divide by the quant rate) and then upscaled back to [0..255] (multiply by the rate).
1204    let depth = depth.clamp(1, 8);
1205    let num_colours = u16::pow(2, depth);
1206    let quant_rate = (256_u16 / num_colours) as u8;
1207    let mut lookup_table: Vec<u8> = vec![0; 256];
1208    for (tbl_idx, table) in lookup_table.iter_mut().enumerate().take(256_usize) {
1209        let downscaled_val = (tbl_idx as u8) / quant_rate;
1210        let upscaled_val = downscaled_val * quant_rate;
1211        *table = upscaled_val.clamp(0, 255);
1212    }
1213
1214    for row in 0..height - 1 {
1215        for col in 0..width - 1 {
1216            for chan in 0..channels - 1 {
1217                let buf_idx = row * row_stride + col * col_stride + chan * chan_stride;
1218                let old_pixel = buf[buf_idx];
1219                let new_pixel = lookup_table[old_pixel as usize];
1220
1221                buf[buf_idx] = new_pixel;
1222
1223                let quant_error = (old_pixel as i16) - (new_pixel as i16);
1224
1225                let buf_idx =
1226                    row * row_stride + (col + 1) * col_stride + chan * chan_stride;
1227                let new_pixel = (buf[buf_idx] as i16) + (quant_error * 7) / 16;
1228                buf[buf_idx] = new_pixel.clamp(0, 255) as u8;
1229
1230                let buf_idx = (row + 1) * row_stride + col * col_stride - col_stride
1231                    + chan * chan_stride;
1232                let new_pixel = (buf[buf_idx] as i16) + (quant_error * 3) / 16;
1233                buf[buf_idx] = new_pixel.clamp(0, 255) as u8;
1234
1235                let buf_idx =
1236                    (row + 1) * row_stride + col * col_stride + chan * chan_stride;
1237                let new_pixel = (buf[buf_idx] as i16) + (quant_error * 5) / 16;
1238                buf[buf_idx] = new_pixel.clamp(0, 255) as u8;
1239
1240                let buf_idx =
1241                    (row + 1) * row_stride + (col + 1) * col_stride + chan * chan_stride;
1242                let new_pixel = (buf[buf_idx] as i16) + quant_error / 16;
1243                buf[buf_idx] = new_pixel.clamp(0, 255) as u8;
1244            }
1245        }
1246    }
1247}
1248
1249fn create_gradient_map(color_a: Rgb, color_b: Rgb) -> Vec<Rgb> {
1250    let mut gradient_map = vec![Rgb::new(0, 0, 0); 256];
1251
1252    for (px, pos) in gradient_map.iter_mut().zip(0_u32..) {
1253        let inv_pos = 256 - pos;
1254
1255        px.r = (((color_a.r as u32) * inv_pos + (color_b.r as u32) * pos) / 256)
1256            .clamp(0, 255) as u8;
1257        px.g = (((color_a.g as u32) * inv_pos + (color_b.g as u32) * pos) / 256)
1258            .clamp(0, 255) as u8;
1259        px.b = (((color_a.b as u32) * inv_pos + (color_b.b as u32) * pos) / 256)
1260            .clamp(0, 255) as u8;
1261    }
1262
1263    gradient_map
1264}
1265
1266#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
1267pub fn duotone(photon_image: &mut PhotonImage, color_a: Rgb, color_b: Rgb) {
1268    let gradient_map = create_gradient_map(color_a, color_b);
1269    let buf = photon_image.raw_pixels.as_mut_slice();
1270
1271    for px in buf.chunks_mut(4) {
1272        // Transform RGB (sRGB) to linear luminance (CIE 1931)
1273        let luma =
1274            (((px[0] as u32) * 2126 + (px[1] as u32) * 7152 + (px[2] as u32) * 722)
1275                / 10000)
1276                .clamp(0, 255);
1277
1278        let mapped_luma = &gradient_map[luma as usize];
1279        px[0] = mapped_luma.r;
1280        px[1] = mapped_luma.g;
1281        px[2] = mapped_luma.b;
1282    }
1283}