photon_rs/
multiple.rs

1//! Image manipulation with multiple images, including adding watermarks, changing backgrounds, etc.,
2
3use crate::channels::color_sim;
4use crate::iter::ImageIterator;
5use crate::{helpers, GenericImage, PhotonImage, Rgb};
6use image::DynamicImage::ImageRgba8;
7use image::Pixel as ImagePixel;
8use image::{DynamicImage, GenericImageView, RgbaImage};
9use palette::{Blend, Gradient, Lab, Lch, LinSrgba, Srgb, Srgba};
10use palette::{FromColor, IntoColor};
11use std::cmp::{max, min};
12
13#[cfg(feature = "enable_wasm")]
14use wasm_bindgen::prelude::*;
15
16/// Add a watermark to an image.
17///
18/// # Arguments
19/// * `img` - A DynamicImage that contains a view into the image.
20/// * `watermark` - The watermark to be placed onto the `img` image.
21/// * `x` - The x coordinate where the watermark's top corner should be positioned.
22/// * `y` - The y coordinate where the watermark's top corner should be positioned.
23/// # Example
24///
25/// ```no_run
26/// // For example, to add a watermark to an image at x: 30, y: 40:
27/// use photon_rs::multiple::watermark;
28/// use photon_rs::native::open_image;
29///
30/// let mut img = open_image("img.jpg").expect("File should open");
31/// let water_mark = open_image("watermark.jpg").expect("File should open");
32/// watermark(&mut img, &water_mark, 30_i64, 40_i64);
33/// ```
34#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
35pub fn watermark(img: &mut PhotonImage, watermark: &PhotonImage, x: i64, y: i64) {
36    let dyn_watermark: DynamicImage = crate::helpers::dyn_image_from_raw(watermark);
37    let mut dyn_img: DynamicImage = crate::helpers::dyn_image_from_raw(img);
38    image::imageops::overlay(&mut dyn_img, &dyn_watermark, x, y);
39    img.raw_pixels = dyn_img.into_bytes();
40}
41
42/// Blend two images together.
43///
44/// The `blend_mode` (3rd param) determines which blending mode to use; change this for varying effects.
45/// The blend modes available include: `overlay`, `over`, `atop`, `xor`, `plus`, `multiply`, `burn`,
46/// `difference`, `soft_light`, `screen`, `hard_light`, `dodge`, `exclusion`, `lighten`, `darken` (more to come)
47/// NOTE: The first image must be smaller than the second image passed as params.
48/// If the first image were larger than the second, then there would be overflowing pixels which would have no corresponding pixels
49/// in the second image.
50/// # Arguments
51/// * `img` - A DynamicImage that contains a view into the image.
52/// * `img2` - The 2nd DynamicImage to be blended with the first.
53/// * `blend_mode` - The blending mode to use. See above for complete list of blend modes available.
54/// # Example
55///
56/// ```no_run
57/// // For example, to blend two images with the `multiply` blend mode:
58/// use photon_rs::multiple::blend;
59/// use photon_rs::native::open_image;
60///
61/// let mut img = open_image("img.jpg").expect("File should open");
62/// let img2 = open_image("img2.jpg").expect("File should open");
63/// blend(&mut img, &img2, "multiply");
64/// ```
65#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
66pub fn blend(
67    photon_image: &mut PhotonImage,
68    photon_image2: &PhotonImage,
69    blend_mode: &str,
70) {
71    let img = crate::helpers::dyn_image_from_raw(photon_image);
72    let img2 = crate::helpers::dyn_image_from_raw(photon_image2);
73
74    let (width, height) = img.dimensions();
75    let (width2, height2) = img2.dimensions();
76
77    if width > width2 || height > height2 {
78        panic!("First image parameter must be smaller than second image parameter. To fix, swap img and img2 params.");
79    }
80    let mut img = img.to_rgba8();
81    let img2 = img2.to_rgba8();
82
83    for (x, y) in ImageIterator::new(width, height) {
84        let pixel = img.get_pixel(x, y);
85        let pixel_img2 = img2.get_pixel(x, y);
86
87        let px_data = pixel.channels();
88        let px_data2 = pixel_img2.channels();
89
90        // let rgb_color: Rgba = Rgba::new(px_data[0] as f32, px_data[1] as f32, px_data[2] as f32, 255.0);
91        // let color: LinSrgba = LinSrgba::from_color(&rgb_color).into_format();
92
93        let color = LinSrgba::new(
94            px_data[0] as f32 / 255.0,
95            px_data[1] as f32 / 255.0,
96            px_data[2] as f32 / 255.0,
97            px_data[3] as f32 / 255.0,
98        )
99        .into_linear();
100
101        let color2 = LinSrgba::new(
102            px_data2[0] as f32 / 255.0,
103            px_data2[1] as f32 / 255.0,
104            px_data2[2] as f32 / 255.0,
105            px_data2[3] as f32 / 255.0,
106        )
107        .into_linear();
108
109        let blended = match blend_mode.to_lowercase().as_str() {
110            // Match a single value
111            "overlay" => color.overlay(color2),
112            "over" => color2.over(color),
113            "atop" => color2.atop(color),
114            "xor" => color2.xor(color),
115            "plus" => color2.plus(color),
116            "multiply" => color2.multiply(color),
117            "burn" => color2.burn(color),
118            "difference" => color2.difference(color),
119            "soft_light" | "soft light" | "softlight" => color2.soft_light(color),
120            "screen" => color2.screen(color),
121            "hard_light" | "hard light" | "hardlight" => color2.hard_light(color),
122            "dodge" => color2.dodge(color),
123            "exclusion" => color2.exclusion(color),
124            "lighten" => color2.lighten(color),
125            "darken" => color2.darken(color),
126            _ => color2.overlay(color),
127        };
128        let components = blended.into_components();
129
130        img.put_pixel(
131            x,
132            y,
133            image::Rgba([
134                (components.0 * 255.0) as u8,
135                (components.1 * 255.0) as u8,
136                (components.2 * 255.0) as u8,
137                (components.3 * 255.0) as u8,
138            ]),
139        );
140    }
141    let dynimage = ImageRgba8(img);
142    photon_image.raw_pixels = dynimage.into_bytes();
143}
144
145// #[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
146// pub fn blend_img_browser(
147//     source_canvas: HtmlCanvasElement,
148//     overlay_img: HtmlImageElement,
149//     blend_mode: &str) {
150//
151//     let ctx = source_canvas
152//     .get_context("2d").unwrap()
153//     .unwrap()
154//     .dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();
155//
156//     ctx.draw_image_with_html_image_element(&overlay_img, 0.0, 0.0);
157//     ctx.set_global_composite_operation(blend_mode);
158//     ctx.set_global_alpha(1.0);
159
160// }
161
162/// Change the background of an image (using a green screen/color screen).
163///
164/// # Arguments
165/// * `img` - A PhotonImage which contains the desired background. Must be the same size as img2.
166/// * `img2` - The image you would like to swap the background of. Must be the same size as img.
167/// * `background_color` - The RGB value of the background, which should be replaced.
168/// # Example
169///
170/// ```no_run
171/// // For example, to replace the background of ImageA (which is RGB value 20, 40, 60) with the background of ImageB:
172/// use photon_rs::Rgb;
173/// use photon_rs::multiple::replace_background;
174/// use photon_rs::native::open_image;
175///
176/// let rgb = Rgb::new(20_u8, 40_u8, 60_u8);
177/// let mut img = open_image("img.jpg").expect("File should open");
178/// let img2 = open_image("img2.jpg").expect("File should open");
179/// replace_background(&mut img, &img2, &rgb);
180/// ```
181pub fn replace_background(
182    photon_image: &mut PhotonImage,
183    img2: &PhotonImage,
184    background_color: &Rgb,
185) {
186    let mut img = helpers::dyn_image_from_raw(photon_image);
187    let img2 = helpers::dyn_image_from_raw(img2);
188
189    for (x, y) in ImageIterator::with_dimension(&img.dimensions()) {
190        let px = img.get_pixel(x, y);
191
192        // Convert the current pixel's colour to the l*a*b colour space
193        let lab: Lab = Srgb::new(
194            background_color.r as f32 / 255.0,
195            background_color.g as f32 / 255.0,
196            background_color.b as f32 / 255.0,
197        )
198        .into_color();
199
200        let channels = px.channels();
201
202        let r_val: f32 = channels[0] as f32 / 255.0;
203        let g_val: f32 = channels[1] as f32 / 255.0;
204        let b_val: f32 = channels[2] as f32 / 255.0;
205
206        let px_lab: Lab = Srgb::new(r_val, g_val, b_val).into_color();
207
208        let sim = color_sim(lab, px_lab);
209
210        // Match
211        if sim < 20 {
212            img.put_pixel(x, y, img2.get_pixel(x, y));
213        } else {
214            img.put_pixel(x, y, px);
215        }
216    }
217    let raw_pixels = img.into_bytes();
218    photon_image.raw_pixels = raw_pixels;
219}
220
221#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
222pub fn create_gradient(width: u32, height: u32) -> PhotonImage {
223    let mut image = RgbaImage::new(width, height);
224
225    // Create a gradient.
226    let grad1 = Gradient::new(vec![
227        LinSrgba::new(1.0, 0.1, 0.1, 1.0),
228        LinSrgba::new(0.1, 0.1, 1.0, 1.0),
229        LinSrgba::new(0.1, 1.0, 0.1, 1.0),
230    ]);
231
232    let _grad3 = Gradient::new(vec![
233        Lch::from_color(LinSrgba::new(1.0, 0.1, 0.1, 1.0)),
234        Lch::from_color(LinSrgba::new(0.1, 0.1, 1.0, 1.0)),
235        Lch::from_color(LinSrgba::new(0.1, 1.0, 0.1, 1.0)),
236    ]);
237
238    for (i, c1) in grad1.take(width as usize).enumerate() {
239        let c1: Srgba<f32> = Srgba::from_linear(c1).into_format();
240        {
241            let mut sub_image = image.sub_image(i as u32, 0, 1, height);
242            for (x, y) in ImageIterator::with_dimension(&sub_image.dimensions()) {
243                let components = c1.into_components();
244                sub_image.put_pixel(
245                    x,
246                    y,
247                    image::Rgba([
248                        (components.0 * 255.0) as u8,
249                        (components.1 * 255.0) as u8,
250                        (components.2 * 255.0) as u8,
251                        255,
252                    ]),
253                );
254            }
255        }
256    }
257    let rgba_img = ImageRgba8(image);
258    let raw_pixels = rgba_img.into_bytes();
259    PhotonImage {
260        raw_pixels,
261        width,
262        height,
263    }
264}
265
266/// Apply a gradient to an image.
267#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
268pub fn apply_gradient(image: &mut PhotonImage) {
269    let gradient = create_gradient(image.width, image.height);
270
271    blend(image, &gradient, "overlay");
272}
273
274/// Build a simple horizontal gradient.
275fn build_horizontal_gradient(
276    width: usize,
277    height: usize,
278    start_x: i32,
279    end_x: i32,
280) -> Vec<f32> {
281    let min_x = min(start_x, end_x);
282    let max_x = max(start_x, end_x);
283    let total_grad_len = max_x - min_x;
284    let total_size = width * height;
285    let mut gradient = std::iter::repeat(0.0).take(total_size).collect::<Vec<_>>();
286    if total_grad_len <= 0 {
287        // Nothing to do. Return a vector filled with zeros.
288        return gradient;
289    }
290
291    // Fill every column from 0 to the leftmost x with zeros.
292    for row in 0..height {
293        for col in 0..min_x {
294            let pos = row * width + col as usize;
295            gradient[pos] = 0.0;
296        }
297    }
298
299    // Fill every column from the rightmost x to image width with ones.
300    // If the rightmost x is less than 0, start with 0.
301    let first_col = max(max_x, 0) as usize;
302    for row in 0..height {
303        for col in first_col..width {
304            let pos = row * width + col;
305            gradient[pos] = 1.0;
306        }
307    }
308
309    // Build gradient between the leftmost and the rightmost x.
310    // Clamp values in such a way that they belong to the visible area.
311    let first_col = max(min_x, 0);
312    let last_col = min(max_x, width as i32);
313    for row in 0..height {
314        for col in first_col..last_col {
315            let pos = row * width + col as usize;
316            let total_len_f32 = total_grad_len as f32;
317            let column_f32 = (col - min_x) as f32;
318            gradient[pos] = (column_f32 / total_len_f32).clamp(0.0, 1.0);
319        }
320    }
321
322    // Inverse values when start_x is on the right.
323    if start_x > end_x {
324        gradient.iter_mut().for_each(|grad| *grad = 1.0 - *grad);
325    }
326
327    gradient
328}
329
330/// Build a simple vertical gradient.
331fn build_vertical_gradient(
332    width: usize,
333    height: usize,
334    start_y: i32,
335    end_y: i32,
336) -> Vec<f32> {
337    let min_y = min(start_y, end_y);
338    let max_y = max(start_y, end_y);
339    let total_grad_len = max_y - min_y;
340    let total_size = width * height;
341    let mut gradient = std::iter::repeat(0.0).take(total_size).collect::<Vec<_>>();
342    if total_grad_len <= 0 {
343        // Nothing to do. Return a vector filled with zeros.
344        return gradient;
345    }
346
347    // Fill every row from 0 to the top y with zeros.
348    for row in 0..min_y {
349        for col in 0..width {
350            let pos = (row as usize) * width + col;
351            gradient[pos] = 0.0;
352        }
353    }
354
355    // Fill every row from the bottom y to image height with ones.
356    // If the bottom y is less than 0, start with 0.
357    let first_row = max(max_y, 0) as usize;
358    for row in first_row..height {
359        for col in 0..width {
360            let pos = row * width + col;
361            gradient[pos] = 1.0;
362        }
363    }
364
365    // Build gradient between the top and the bottom y.
366    // Clamp values in such a way that they belong to the visible area.
367    let first_row = max(min_y, 0);
368    let last_row = min(max_y, height as i32);
369    for row in first_row..last_row {
370        for col in 0..width {
371            let pos = (row as usize) * width + col;
372            let total_len_f32 = total_grad_len as f32;
373            let row_f32 = (row - min_y) as f32;
374            gradient[pos] = (row_f32 / total_len_f32).clamp(0.0, 1.0);
375        }
376    }
377
378    // Inverse values when start_y is at the bottom.
379    if start_y > end_y {
380        gradient.iter_mut().for_each(|grad| *grad = 1.0 - *grad);
381    }
382
383    gradient
384}
385
386/// Build an axial gradient.
387fn build_axial_gradient(
388    width: usize,
389    height: usize,
390    start_x: i32,
391    end_x: i32,
392    start_y: i32,
393    end_y: i32,
394) -> Vec<f32> {
395    let len_x = (end_x - start_x) as f32;
396    let len_y = (end_y - start_y) as f32;
397    let total_grad_len = (len_x * len_x + len_y * len_y).sqrt();
398
399    let total_size = width * height;
400    let mut gradient = std::iter::repeat(0.0).take(total_size).collect::<Vec<_>>();
401    if total_grad_len <= 0.0 {
402        // Nothing to do. Return a vector filled with zeros.
403        return gradient;
404    }
405
406    let min_x = min(start_x, end_x) as f32;
407    let max_x = max(start_x, end_x) as f32;
408    let min_y = min(start_y, end_y) as f32;
409    let max_y = max(start_y, end_y) as f32;
410    let len_x_sq = len_x * len_x;
411    let len_y_sq = len_y * len_y;
412    let start_x_f32 = start_x as f32;
413    let start_y_f32 = start_y as f32;
414
415    // Build gradient between start_x, end_x, start_y and end_y.
416    // The idea is to find the foot of perpendicular from each point to the gradient line.
417    // If the foot belongs to the gradient line, find the distance from (start_x, start_y)
418    // to the foot point and divide it by total gradient length.
419    // If the foot exceeds gradient line bounds, fill it with zeros or ones in accordance with
420    // the direction of gradient vector.
421    for row in 0..height {
422        for col in 0..width {
423            let pos = row * width + col;
424            let col_f32 = col as f32;
425            let row_f32 = row as f32;
426            let foot_x = (start_x_f32 * len_y_sq
427                + col_f32 * len_x_sq
428                + len_x * len_y * (row_f32 - start_y_f32))
429                / (len_y_sq + len_x_sq);
430            let foot_y = (len_x * (col_f32 - foot_x)) / len_y + row_f32;
431
432            // Check that found coordinates do not exceed gradient bounds.
433            if min_x <= foot_x && foot_x <= max_x && min_y <= foot_y && foot_y <= max_y {
434                let norm_x = foot_x - start_x_f32;
435                let norm_y = foot_y - start_y_f32;
436                let grad_dist = (norm_x * norm_x + norm_y * norm_y).sqrt();
437                let total_len_f32 = total_grad_len;
438                gradient[pos] = (grad_dist / total_len_f32).clamp(0.0, 1.0);
439            } else {
440                let fill_bottom_right =
441                    start_x < end_x && start_y < end_y && foot_x > max_x;
442                let fill_bottom_left =
443                    start_x > end_x && start_y < end_y && foot_x < min_x;
444                let fill_top_right =
445                    start_x < end_x && start_y > end_y && foot_y < min_y;
446                let fill_top_left = start_x > end_x && start_y > end_y && foot_y < min_y;
447                if fill_bottom_right
448                    || fill_bottom_left
449                    || fill_top_right
450                    || fill_top_left
451                {
452                    gradient[pos] = 1.0;
453                }
454            }
455        }
456    }
457
458    gradient
459}
460
461/// Fades one image into another.
462///
463/// For horizontal fading, set both `start_y` and `end_y` to the same value.
464/// For vertical fading, set both `start_x` and `end_x` to the same value.
465/// Otherwise, axial fading is applied.
466///
467/// # Arguments
468/// * `img1` - Image to fade from. Must be the same size as img2.
469/// * `img2` - Image to fade to. Must be the same size as img1.
470/// * `start_x` - Column where the fading begins.
471/// * `end_x` - Column where the fading ends.
472/// * `start_y` - Row where the fading begins.
473/// * `end_y` - Row where the fading ends.
474/// # Example
475///
476/// ```no_run
477/// use photon_rs::multiple::fade;
478/// use photon_rs::native::open_image;
479///
480/// let img1 = open_image("img1.jpg").expect("File should open");
481/// let img2 = open_image("img2.jpg").expect("File should open");
482/// let _faded_img = fade(&img1, &img2, 0, 100, 0, 100);
483/// ```
484pub fn fade(
485    img1: &PhotonImage,
486    img2: &PhotonImage,
487    start_x: i32,
488    end_x: i32,
489    start_y: i32,
490    end_y: i32,
491) -> PhotonImage {
492    if img1.width != img2.width || img1.height != img2.height {
493        panic!("Images must have the same size.");
494    }
495
496    let width = img1.width as usize;
497    let height = img1.height as usize;
498
499    let buf_img1 = &img1.raw_pixels;
500    let buf_img2 = &img2.raw_pixels;
501    let mut buf_res = Vec::with_capacity(width * height * 4);
502
503    // Determine, which gradient must be built.
504    let gradient = if end_y == start_y {
505        build_horizontal_gradient(width, height, start_x, end_x)
506    } else if start_x == end_x {
507        build_vertical_gradient(width, height, start_y, end_y)
508    } else {
509        build_axial_gradient(width, height, start_x, end_x, start_y, end_y)
510    };
511
512    for row in 0..height {
513        for col in 0..width {
514            let grad_idx = row * width + col;
515            let opacity_img1 = gradient[grad_idx];
516            let opacity_img2 = 1.0 - opacity_img1;
517
518            let buf_idx = row * width * 4 + col * 4;
519
520            let img1_r = buf_img1[buf_idx] as f32;
521            let img1_g = buf_img1[buf_idx + 1] as f32;
522            let img1_b = buf_img1[buf_idx + 2] as f32;
523
524            let img2_r = buf_img2[buf_idx] as f32;
525            let img2_g = buf_img2[buf_idx + 1] as f32;
526            let img2_b = buf_img2[buf_idx + 2] as f32;
527
528            let res_r = ((img1_r * opacity_img1) + (img2_r * opacity_img2))
529                .clamp(0.0, 255.0) as u8;
530            let res_g = ((img1_g * opacity_img1) + (img2_g * opacity_img2))
531                .clamp(0.0, 255.0) as u8;
532            let res_b = ((img1_b * opacity_img1) + (img2_b * opacity_img2))
533                .clamp(0.0, 255.0) as u8;
534
535            // Set alpha channel to 100%.
536            let res_a = 255;
537
538            buf_res.push(res_r);
539            buf_res.push(res_g);
540            buf_res.push(res_b);
541            buf_res.push(res_a);
542        }
543    }
544
545    PhotonImage::new(buf_res, img1.width, img1.height)
546}