photon_rs/
transform.rs

1//! Image transformations, ie: scale, crop, resize, etc.,
2
3use crate::helpers;
4use crate::iter::ImageIterator;
5use crate::{PhotonImage, Rgba};
6use image::imageops::FilterType;
7use image::DynamicImage::ImageRgba8;
8use image::{GenericImageView, ImageBuffer, Pixel, RgbaImage};
9use std::cmp::min;
10
11#[cfg(feature = "enable_wasm")]
12use wasm_bindgen::prelude::*;
13#[cfg(all(feature = "enable_wasm", target_arch = "wasm32"))]
14use wasm_bindgen::{Clamped, JsCast};
15#[cfg(all(feature = "enable_wasm", target_arch = "wasm32"))]
16use web_sys::{HtmlCanvasElement, ImageData};
17
18/// Crop an image.
19///
20/// # Arguments
21/// * `img` - A PhotonImage.
22///
23/// # Example
24///
25/// ```no_run
26/// // For example, to crop an image at (0, 0) to (500, 800)
27/// use photon_rs::native::{open_image};
28/// use photon_rs::transform::crop;
29/// use photon_rs::PhotonImage;
30///
31/// let mut img = open_image("img.jpg").expect("File should open");
32/// let cropped_img: PhotonImage = crop(&img, 0_u32, 0_u32, 500_u32, 800_u32);
33/// // Write the contents of this image in JPG format.
34/// ```
35#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
36pub fn crop(
37    photon_image: &PhotonImage,
38    x1: u32,
39    y1: u32,
40    x2: u32,
41    y2: u32,
42) -> PhotonImage {
43    let img = helpers::dyn_image_from_raw(photon_image);
44
45    let mut cropped_img: RgbaImage = ImageBuffer::new(x2 - x1, y2 - y1);
46
47    for (x, y) in ImageIterator::with_dimension(&cropped_img.dimensions()) {
48        let px = img.get_pixel(x1 + x, y1 + y);
49        cropped_img.put_pixel(x, y, px);
50    }
51    let dynimage = ImageRgba8(cropped_img);
52
53    let width = dynimage.width();
54    let height = dynimage.height();
55
56    let raw_pixels = dynimage.into_bytes();
57    PhotonImage {
58        raw_pixels,
59        width,
60        height,
61    }
62}
63
64#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
65#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
66pub fn crop_img_browser(
67    source_canvas: HtmlCanvasElement,
68    width: f64,
69    height: f64,
70    left: f64,
71    top: f64,
72) -> HtmlCanvasElement {
73    let document = web_sys::window().unwrap().document().unwrap();
74    let dest_canvas = document
75        .create_element("canvas")
76        .unwrap()
77        .dyn_into::<web_sys::HtmlCanvasElement>()
78        .unwrap();
79
80    dest_canvas.set_width(width as u32);
81    dest_canvas.set_height(height as u32);
82
83    let ctx = dest_canvas
84        .get_context("2d")
85        .unwrap()
86        .unwrap()
87        .dyn_into::<web_sys::CanvasRenderingContext2d>()
88        .unwrap();
89
90    ctx.draw_image_with_html_canvas_element_and_sw_and_sh_and_dx_and_dy_and_dw_and_dh(
91        &source_canvas,
92        left,
93        top,
94        width,
95        height,
96        0.0,
97        0.0,
98        width,
99        height,
100    )
101    .unwrap();
102
103    dest_canvas
104}
105
106/// Flip an image horizontally.
107///
108/// # Arguments
109/// * `img` - A PhotonImage.
110///
111/// # Example
112///
113/// ```no_run
114/// // For example, to flip an image horizontally:
115/// use photon_rs::native::open_image;
116/// use photon_rs::transform::fliph;
117///
118/// let mut img = open_image("img.jpg").expect("File should open");
119/// fliph(&mut img);
120/// ```
121#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
122pub fn fliph(photon_image: &mut PhotonImage) {
123    let img = helpers::dyn_image_from_raw(photon_image);
124
125    let width = img.width();
126    let height = img.height();
127    let mut flipped_img: RgbaImage = ImageBuffer::new(width, height);
128
129    for (x, y) in ImageIterator::new(width, height) {
130        let px = img.get_pixel(x, y);
131        flipped_img.put_pixel(width - x - 1, y, px);
132    }
133
134    let dynimage = ImageRgba8(flipped_img);
135    let raw_pixels = dynimage.into_bytes();
136    photon_image.raw_pixels = raw_pixels;
137}
138
139/// Flip an image vertically.
140///
141/// # Arguments
142/// * `img` - A PhotonImage.
143///
144/// # Example
145///
146/// ```no_run
147/// // For example, to flip an image vertically:
148/// use photon_rs::native::open_image;
149/// use photon_rs::transform::flipv;
150///
151/// let mut img = open_image("img.jpg").expect("File should open");
152/// flipv(&mut img);
153/// ```
154#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
155pub fn flipv(photon_image: &mut PhotonImage) {
156    let img = helpers::dyn_image_from_raw(photon_image);
157
158    let width = img.width();
159    let height = img.height();
160
161    let mut flipped_img: RgbaImage = ImageBuffer::new(width, height);
162
163    for (x, y) in ImageIterator::new(width, height) {
164        let px = img.get_pixel(x, y);
165        flipped_img.put_pixel(x, height - y - 1, px);
166    }
167
168    let dynimage = ImageRgba8(flipped_img);
169    let raw_pixels = dynimage.into_bytes();
170    photon_image.raw_pixels = raw_pixels;
171}
172
173#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
174pub enum SamplingFilter {
175    Nearest = 1,
176    Triangle = 2,
177    CatmullRom = 3,
178    Gaussian = 4,
179    Lanczos3 = 5,
180}
181
182fn filter_type_from_sampling_filter(sampling_filter: SamplingFilter) -> FilterType {
183    match sampling_filter {
184        SamplingFilter::Nearest => FilterType::Nearest,
185        SamplingFilter::Triangle => FilterType::Triangle,
186        SamplingFilter::CatmullRom => FilterType::CatmullRom,
187        SamplingFilter::Gaussian => FilterType::Gaussian,
188        SamplingFilter::Lanczos3 => FilterType::Lanczos3,
189    }
190}
191
192/// Resize an image on the web.
193///
194/// # Arguments
195/// * `img` - A PhotonImage.
196/// * `width` - New width.
197/// * `height` - New height.
198/// * `sampling_filter` - Nearest = 1, Triangle = 2, CatmullRom = 3, Gaussian = 4, Lanczos3 = 5
199#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
200#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
201pub fn resize_img_browser(
202    photon_img: &PhotonImage,
203    width: u32,
204    height: u32,
205    sampling_filter: SamplingFilter,
206) -> HtmlCanvasElement {
207    let sampling_filter = filter_type_from_sampling_filter(sampling_filter);
208    let dyn_img = helpers::dyn_image_from_raw(photon_img);
209    let resized_img = ImageRgba8(image::imageops::resize(
210        &dyn_img,
211        width,
212        height,
213        sampling_filter,
214    ));
215
216    // TODO Check if in browser or Node.JS
217    let document = web_sys::window().unwrap().document().unwrap();
218    let canvas = document
219        .create_element("canvas")
220        .unwrap()
221        .dyn_into::<web_sys::HtmlCanvasElement>()
222        .unwrap();
223
224    let width = resized_img.width();
225    let height = resized_img.height();
226
227    canvas.set_width(width);
228    canvas.set_height(width);
229
230    let new_img_data = ImageData::new_with_u8_clamped_array_and_sh(
231        Clamped(&mut resized_img.into_bytes()),
232        width,
233        height,
234    );
235
236    let ctx = canvas
237        .get_context("2d")
238        .unwrap()
239        .unwrap()
240        .dyn_into::<web_sys::CanvasRenderingContext2d>()
241        .unwrap();
242
243    // Place the new imagedata onto the canvas
244    ctx.put_image_data(&new_img_data.unwrap(), 0.0, 0.0)
245        .unwrap();
246
247    canvas
248}
249
250/// Resize an image.
251///
252/// # Arguments
253/// * `img` - A PhotonImage.
254/// * `width` - New width.
255/// * `height` - New height.
256/// * `sampling_filter` - Nearest = 1, Triangle = 2, CatmullRom = 3, Gaussian = 4, Lanczos3 = 5
257#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
258pub fn resize(
259    photon_img: &PhotonImage,
260    width: u32,
261    height: u32,
262    sampling_filter: SamplingFilter,
263) -> PhotonImage {
264    let sampling_filter = filter_type_from_sampling_filter(sampling_filter);
265
266    let dyn_img = helpers::dyn_image_from_raw(photon_img);
267    let resized_img = ImageRgba8(image::imageops::resize(
268        &dyn_img,
269        width,
270        height,
271        sampling_filter,
272    ));
273
274    let width = resized_img.width();
275    let height = resized_img.height();
276
277    PhotonImage {
278        raw_pixels: resized_img.into_bytes(),
279        width,
280        height,
281    }
282}
283
284/// Resize image using seam carver.
285/// Resize only if new dimensions are smaller, than original image.
286/// # NOTE: This is still experimental feature, and pretty slow.
287///
288/// # Arguments
289/// * `img` - A PhotonImage.
290/// * `width` - New width.
291/// * `height` - New height.
292///
293/// # Example
294///
295/// ```no_run
296/// // For example, resize image using seam carver:
297/// use photon_rs::native::open_image;
298/// use photon_rs::transform::seam_carve;
299/// use photon_rs::PhotonImage;
300///
301/// let img = open_image("img.jpg").expect("File should open");
302/// let result: PhotonImage = seam_carve(&img, 100_u32, 100_u32);
303/// ```
304#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
305pub fn seam_carve(img: &PhotonImage, width: u32, height: u32) -> PhotonImage {
306    let mut img: RgbaImage = ImageBuffer::from_raw(
307        img.get_width(),
308        img.get_height(),
309        img.raw_pixels.to_vec(),
310    )
311    .unwrap();
312    let (w, h) = img.dimensions();
313    let (diff_w, diff_h) = (w - w.min(width), h - h.min(height));
314
315    for _ in 0..diff_w {
316        let vec_steam = imageproc::seam_carving::find_vertical_seam(&img);
317        img = imageproc::seam_carving::remove_vertical_seam(&img, &vec_steam);
318    }
319    if diff_h.ne(&0_u32) {
320        img = image::imageops::rotate90(&img);
321        for _ in 0..diff_h {
322            let vec_steam = imageproc::seam_carving::find_vertical_seam(&img);
323            img = imageproc::seam_carving::remove_vertical_seam(&img, &vec_steam);
324        }
325        img = image::imageops::rotate270(&img);
326    }
327
328    let img = ImageRgba8(img);
329
330    let width = img.width();
331    let height = img.height();
332
333    PhotonImage {
334        raw_pixels: img.into_bytes(),
335        width,
336        height,
337    }
338}
339
340/// Shear the image along the X axis.
341/// A sheared PhotonImage is returned.
342///
343/// # Arguments
344/// * `img` - A PhotonImage. See the PhotonImage struct for details.
345/// * `shear` - Amount to shear.
346///
347/// # Example
348///
349/// ```no_run
350/// // For example, to shear an image by 0.5:
351/// use photon_rs::native::open_image;
352/// use photon_rs::transform::shearx;
353///
354/// let img = open_image("img.jpg").expect("File should open");
355/// let sheared_img = shearx(&img, 0.5);
356/// ```
357#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
358pub fn shearx(photon_img: &PhotonImage, shear: f32) -> PhotonImage {
359    let img = helpers::dyn_image_from_raw(photon_img);
360    let src_width = img.width();
361    let src_height = img.height();
362
363    let maxskew = shear * (src_height as f32);
364    let dst_width = maxskew.floor().abs() as u32 + src_width;
365
366    let mut delta = 0;
367    if shear < 0. {
368        delta = dst_width - src_width;
369    }
370
371    let mut sheared_image: RgbaImage = ImageBuffer::new(dst_width, src_height);
372
373    for old_y in 0..src_height {
374        let skew = shear * (old_y as f32 + 0.5);
375        let skewi = skew.floor() as i32 + delta as i32;
376        let skewf = skew.fract().abs();
377        let mut oleft = image::Rgba([0_u8, 0_u8, 0_u8, 0_u8]);
378        for old_x in (0..src_width).rev() {
379            let mut pixel = img.get_pixel(old_x, old_y);
380            let mut left = pixel.map(|val| (val as f32 * skewf) as u8);
381            if shear >= 0. {
382                left = pixel.map2(&left, |val1, val2| val1 - val2);
383            }
384            pixel = pixel.map2(&left, |val1, val2| val1 - val2);
385            pixel = pixel.map2(&oleft, |val1, val2| {
386                min(val1 as u16 + val2 as u16, 255_u16) as u8
387            });
388            let new_x = (old_x as i32 + skewi) as u32;
389            sheared_image.put_pixel(new_x, old_y, pixel);
390            oleft = left;
391        }
392        sheared_image.put_pixel(skewi as u32, old_y, oleft);
393    }
394
395    let dynimage = ImageRgba8(sheared_image);
396    let width = dynimage.width();
397    let height = dynimage.height();
398    let raw_pixels = dynimage.into_bytes();
399
400    PhotonImage::new(raw_pixels, width, height)
401}
402
403/// Shear the image along the Y axis.
404/// A sheared PhotonImage is returned.
405///
406/// # Arguments
407/// * `img` - A PhotonImage. See the PhotonImage struct for details.
408/// * `shear` - Amount to shear.
409///
410/// # Example
411///
412/// ```no_run
413/// // For example, to shear an image by 0.5:
414/// use photon_rs::native::open_image;
415/// use photon_rs::transform::sheary;
416///
417/// let img = open_image("img.jpg").expect("File should open");
418/// let sheared_img = sheary(&img, 0.5);
419/// ```
420#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
421pub fn sheary(photon_img: &PhotonImage, shear: f32) -> PhotonImage {
422    let img = helpers::dyn_image_from_raw(photon_img);
423    let src_width = img.width();
424    let src_height = img.height();
425
426    let maxskew = shear * (src_width as f32);
427    let dst_height = maxskew.floor().abs() as u32 + src_height;
428
429    let mut delta = 0;
430    if shear < 0. {
431        delta = dst_height - src_height;
432    }
433
434    let mut sheared_image: RgbaImage = ImageBuffer::new(src_width, dst_height);
435
436    for old_x in 0..src_width {
437        let skew = shear * (old_x as f32 + 0.5);
438        let skewi = skew.floor() as i32 + delta as i32;
439        let skewf = skew.fract().abs();
440        let mut oleft = image::Rgba([0_u8, 0_u8, 0_u8, 0_u8]);
441        for old_y in (0..src_height).rev() {
442            let mut pixel = img.get_pixel(old_x, old_y);
443            let mut left = pixel.map(|val| (val as f32 * skewf).floor() as u8);
444            if shear >= 0. {
445                left = pixel.map2(&left, |val1, val2| val1 - val2);
446            }
447            pixel = pixel.map2(&left, |val1, val2| val1 - val2);
448            pixel = pixel.map2(&oleft, |val1, val2| {
449                min(val1 as u16 + val2 as u16, 255_u16) as u8
450            });
451            let new_y = (old_y as i32 + skewi) as u32;
452            sheared_image.put_pixel(old_x, new_y, pixel);
453            oleft = left;
454        }
455        sheared_image.put_pixel(old_x, skewi as u32, oleft);
456    }
457
458    let dynimage = ImageRgba8(sheared_image);
459    let width = dynimage.width();
460    let height = dynimage.height();
461    let raw_pixels = dynimage.into_bytes();
462
463    PhotonImage::new(raw_pixels, width, height)
464}
465
466/// Apply uniform padding around the PhotonImage
467/// A padded PhotonImage is returned.
468/// # Arguments
469/// * `img` - A PhotonImage. See the PhotonImage struct for details.
470/// * `padding` - The amount of padding to be applied to the PhotonImage.
471/// * `padding_rgba` - Tuple containing the RGBA code for padding color.
472///
473/// # Example
474///
475/// ```no_run
476/// // For example, to apply a padding of 10 pixels around a PhotonImage:
477/// use photon_rs::transform::padding_uniform;
478/// use photon_rs::native::open_image;
479/// use photon_rs::Rgba;
480///
481/// let mut img = open_image("img.jpg").expect("File should open");
482/// let rgba = Rgba::new(200_u8, 100_u8, 150_u8, 255_u8);
483/// padding_uniform(&img, 10_u32, rgba);
484/// ```
485#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
486pub fn padding_uniform(
487    img: &PhotonImage,
488    padding: u32,
489    padding_rgba: Rgba,
490) -> PhotonImage {
491    let image_buffer = img.get_raw_pixels();
492    let img_width = img.get_width();
493    let img_height = img.get_height();
494
495    let mut img_padded_buffer = Vec::<u8>::new();
496    let width_padded: u32 = img_width + 2 * padding;
497    let height_padded: u32 = img_height + 2 * padding;
498
499    for _ in 0..((width_padded + 1) * padding) {
500        img_padded_buffer.push(padding_rgba.get_red());
501        img_padded_buffer.push(padding_rgba.get_green());
502        img_padded_buffer.push(padding_rgba.get_blue());
503        img_padded_buffer.push(padding_rgba.get_alpha());
504    }
505
506    for i in (0..image_buffer.len()).step_by(4) {
507        if (i / 4) % img_width as usize == 0 && i != 0 {
508            for _ in (0..2 * padding).step_by(1) {
509                img_padded_buffer.push(padding_rgba.get_red());
510                img_padded_buffer.push(padding_rgba.get_green());
511                img_padded_buffer.push(padding_rgba.get_blue());
512                img_padded_buffer.push(padding_rgba.get_alpha());
513            }
514        }
515        img_padded_buffer.push(image_buffer[i]);
516        img_padded_buffer.push(image_buffer[i + 1]);
517        img_padded_buffer.push(image_buffer[i + 2]);
518        img_padded_buffer.push(image_buffer[i + 3]);
519    }
520
521    for _ in 0..((width_padded + 1) * padding) {
522        img_padded_buffer.push(padding_rgba.get_red());
523        img_padded_buffer.push(padding_rgba.get_green());
524        img_padded_buffer.push(padding_rgba.get_blue());
525        img_padded_buffer.push(padding_rgba.get_alpha());
526    }
527
528    PhotonImage::new(img_padded_buffer, width_padded, height_padded)
529}
530
531/// Apply padding on the left side of the PhotonImage
532/// A padded PhotonImage is returned.
533/// # Arguments
534/// * `img` - A PhotonImage. See the PhotonImage struct for details.
535/// * `padding` - The amount of padding to be applied to the PhotonImage.
536/// * `padding_rgba` - Tuple containing the RGBA code for padding color.
537///
538/// # Example
539///
540/// ```no_run
541/// // For example, to apply a padding of 10 pixels on the left side of a PhotonImage:
542/// use photon_rs::transform::padding_left;
543/// use photon_rs::native::open_image;
544/// use photon_rs::Rgba;
545///
546/// let mut img = open_image("img.jpg").expect("File should open");
547/// let rgba = Rgba::new(200_u8, 100_u8, 150_u8, 255_u8);
548/// padding_left(&img, 10_u32, rgba);
549/// ```
550#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
551pub fn padding_left(img: &PhotonImage, padding: u32, padding_rgba: Rgba) -> PhotonImage {
552    let image_buffer = img.get_raw_pixels();
553    let img_width = img.get_width();
554    let img_height = img.get_height();
555
556    let mut img_padded_buffer = Vec::<u8>::new();
557    let width_padded: u32 = img_width + padding;
558
559    for i in 0..img_height as usize {
560        let img_slice = image_buffer
561            [(i * 4 * img_width as usize)..(i + 1) * 4 * img_width as usize]
562            .to_owned();
563
564        for _ in 0..padding {
565            img_padded_buffer.push(padding_rgba.get_red());
566            img_padded_buffer.push(padding_rgba.get_green());
567            img_padded_buffer.push(padding_rgba.get_blue());
568            img_padded_buffer.push(padding_rgba.get_alpha());
569        }
570        for x in img_slice {
571            img_padded_buffer.push(x);
572        }
573    }
574    PhotonImage::new(img_padded_buffer, width_padded, img_height)
575}
576
577/// Apply padding on the left side of the PhotonImage
578/// A padded PhotonImage is returned.
579/// # Arguments
580/// * `img` - A PhotonImage. See the PhotonImage struct for details.
581/// * `padding` - The amount of padding to be applied to the PhotonImage.
582/// * `padding_rgba` - Tuple containing the RGBA code for padding color.
583///
584/// # Example
585///
586/// ```no_run
587/// // For example, to apply a padding of 10 pixels on the right side of a PhotonImage:
588/// use photon_rs::transform::padding_right;
589/// use photon_rs::native::open_image;
590/// use photon_rs::Rgba;
591///
592/// let mut img = open_image("img.jpg").expect("File should open");
593/// let rgba = Rgba::new(200_u8, 100_u8, 150_u8, 255_u8);
594/// padding_right(&img, 10_u32, rgba);
595/// ```
596#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
597pub fn padding_right(
598    img: &PhotonImage,
599    padding: u32,
600    padding_rgba: Rgba,
601) -> PhotonImage {
602    let image_buffer = img.get_raw_pixels();
603    let img_width = img.get_width();
604    let img_height = img.get_height();
605
606    let mut img_padded_buffer = Vec::<u8>::new();
607    let width_padded: u32 = img_width + padding;
608
609    for i in 0..img_height as usize {
610        let img_slice = image_buffer
611            [(i * 4 * img_width as usize)..(i + 1) * 4 * img_width as usize]
612            .to_owned();
613        for x in img_slice {
614            img_padded_buffer.push(x);
615        }
616        for _ in 0..padding {
617            img_padded_buffer.push(padding_rgba.get_red());
618            img_padded_buffer.push(padding_rgba.get_green());
619            img_padded_buffer.push(padding_rgba.get_blue());
620            img_padded_buffer.push(padding_rgba.get_alpha());
621        }
622    }
623    PhotonImage::new(img_padded_buffer, width_padded, img_height)
624}
625
626/// Apply padding on the left side of the PhotonImage
627/// A padded PhotonImage is returned.
628/// # Arguments
629/// * `img` - A PhotonImage. See the PhotonImage struct for details.
630/// * `padding` - The amount of padding to be applied to the PhotonImage.
631/// * `padding_rgba` - Tuple containing the RGBA code for padding color.
632///
633/// # Example
634///
635/// ```no_run
636/// // For example, to apply a padding of 10 pixels on the top of a PhotonImage:
637/// use photon_rs::transform::padding_top;
638/// use photon_rs::native::open_image;
639/// use photon_rs::Rgba;
640///
641/// let mut img = open_image("img.jpg").expect("File should open");
642/// let rgba = Rgba::new(200_u8, 100_u8, 150_u8, 255_u8);
643/// padding_top(&img, 10_u32, rgba);
644/// ```
645#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
646pub fn padding_top(img: &PhotonImage, padding: u32, padding_rgba: Rgba) -> PhotonImage {
647    let image_buffer = img.get_raw_pixels();
648    let img_width = img.get_width();
649    let img_height = img.get_height();
650
651    let height_padded: u32 = img_height + padding;
652    let mut img_padded_buffer: Vec<u8> = Vec::<u8>::new();
653
654    for _ in 0..(padding * img_width) {
655        img_padded_buffer.push(padding_rgba.get_red());
656        img_padded_buffer.push(padding_rgba.get_green());
657        img_padded_buffer.push(padding_rgba.get_blue());
658        img_padded_buffer.push(padding_rgba.get_alpha());
659    }
660
661    for i in (0..image_buffer.len()).step_by(4) {
662        img_padded_buffer.push(image_buffer[i]);
663        img_padded_buffer.push(image_buffer[i + 1]);
664        img_padded_buffer.push(image_buffer[i + 2]);
665        img_padded_buffer.push(image_buffer[i + 3]);
666    }
667
668    PhotonImage::new(img_padded_buffer, img_width, height_padded)
669}
670
671/// Apply padding on the left side of the PhotonImage
672/// A padded PhotonImage is returned.
673/// # Arguments
674/// * `img` - A PhotonImage. See the PhotonImage struct for details.
675/// * `padding` - The amount of padding to be applied to the PhotonImage.
676/// * `padding_rgba` - Tuple containing the RGBA code for padding color.
677///
678/// # Example
679///
680/// ```no_run
681/// // For example, to apply a padding of 10 pixels on the bottom of a PhotonImage:
682/// use photon_rs::transform::padding_bottom;
683/// use photon_rs::native::open_image;
684/// use photon_rs::Rgba;
685///
686/// let mut img = open_image("img.jpg").expect("File should open");
687/// let rgba = Rgba::new(200_u8, 100_u8, 150_u8, 255_u8);
688/// padding_bottom(&img, 10_u32, rgba);
689/// ```
690#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
691pub fn padding_bottom(
692    img: &PhotonImage,
693    padding: u32,
694    padding_rgba: Rgba,
695) -> PhotonImage {
696    let image_buffer = img.get_raw_pixels();
697    let img_width = img.get_width();
698    let img_height = img.get_height();
699
700    let height_padded: u32 = img_height + padding;
701    let mut img_padded_buffer: Vec<u8> = Vec::<u8>::new();
702
703    for i in (0..image_buffer.len()).step_by(4) {
704        img_padded_buffer.push(image_buffer[i]);
705        img_padded_buffer.push(image_buffer[i + 1]);
706        img_padded_buffer.push(image_buffer[i + 2]);
707        img_padded_buffer.push(image_buffer[i + 3]);
708    }
709
710    for _ in 0..(padding * img_width) {
711        img_padded_buffer.push(padding_rgba.get_red());
712        img_padded_buffer.push(padding_rgba.get_green());
713        img_padded_buffer.push(padding_rgba.get_blue());
714        img_padded_buffer.push(padding_rgba.get_alpha());
715    }
716
717    PhotonImage::new(img_padded_buffer, img_width, height_padded)
718}
719
720/// Rotate the PhotonImage on an arbitrary angle
721/// A rotated PhotonImage is returned.
722///
723/// # Arguments
724/// * `img` - A PhotonImage. See the PhotonImage struct for details.
725/// * `angle` - Rotation angle in degrees.
726///
727/// # Example
728///
729/// ```no_run
730/// // For example, to rotate a PhotonImage by 30 degrees:
731/// use photon_rs::native::open_image;
732/// use photon_rs::transform::rotate;
733///
734/// let img = open_image("img.jpg").expect("File should open");
735/// let rotated_img = rotate(&img, 30.0);
736/// ```
737#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
738#[must_use]
739pub fn rotate(photon_img: &PhotonImage, angle: f32) -> PhotonImage {
740    // 390, 750 and 30 degrees represent the same angle. Trim 360.
741    let full_circle_count = angle as i32 / 360;
742    let normalized_angle = angle as i32 - full_circle_count * 360;
743    if normalized_angle == 0 {
744        return photon_img.clone();
745    }
746
747    // Handle negative angles. -80 describes the same angle as 360 - 80 = 280.
748    let positive_angle = if normalized_angle < 0 {
749        normalized_angle + 360
750    } else {
751        normalized_angle
752    };
753
754    let right_angle_count = positive_angle / 90;
755    let mut rgba_img: RgbaImage = ImageBuffer::from_raw(
756        photon_img.get_width(),
757        photon_img.get_height(),
758        photon_img.get_raw_pixels().to_vec(),
759    )
760    .unwrap();
761    for _ in 0..right_angle_count {
762        rgba_img = image::imageops::rotate90(&rgba_img);
763    }
764
765    let dynimage = ImageRgba8(rgba_img);
766    let src_width = dynimage.width();
767    let src_height = dynimage.height();
768    let raw_pixels = dynimage.into_bytes();
769
770    let mut img_out = PhotonImage::new(raw_pixels, src_width, src_height);
771
772    let theta = ((angle % 360.) - (right_angle_count * 90) as f32).to_radians();
773    let beta = theta.sin();
774    let alpha = -1. * ((theta / 2.).tan());
775
776    img_out = shearx(&img_out, alpha);
777    img_out = sheary(&img_out, beta);
778    img_out = shearx(&img_out, alpha);
779
780    img_out
781}
782
783fn greatest_common_divisor(left_val: usize, right_val: usize) -> usize {
784    let mut a = left_val;
785    let mut b = right_val;
786    while b > 0 {
787        let t = b;
788        b = a % b;
789        a = t;
790    }
791    a
792}
793
794fn copy_row(buf: &[u8], row_pos: usize, row_stride: usize) -> Vec<u8> {
795    let mut result = Vec::<u8>::new();
796    for byte_idx in 0..row_stride {
797        let src_idx = (row_pos * row_stride) + byte_idx;
798        result.push(buf[src_idx]);
799    }
800
801    result
802}
803
804/// Resample the PhotonImage.
805///
806/// # Arguments
807/// * `img` - A PhotonImage. See the PhotonImage struct for details.
808/// * `dst_width` - Target width.
809/// * `dst_height` - Target height.
810///
811/// # Example
812///
813/// ```no_run
814/// // For example, to resample a PhotonImage to 1920x1080 size:
815/// use photon_rs::native::open_image;
816/// use photon_rs::transform::resample;
817///
818/// let img = open_image("img.jpg").expect("File should open");
819/// let rotated_img = resample(&img, 1920, 1080);
820/// ```
821#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
822pub fn resample(img: &PhotonImage, dst_width: usize, dst_height: usize) -> PhotonImage {
823    let mut pix_buf = Vec::<u8>::new();
824    if dst_width == 0 || dst_height == 0 {
825        return PhotonImage::new(pix_buf, dst_width as u32, dst_height as u32);
826    }
827
828    let src_width = img.get_width() as usize;
829    let src_height = img.get_height() as usize;
830
831    // The idea is to upsample source width to the greatest commond divisor, then downsample to
832    // target width. For example: resample 240 to 320. The greatest common divisor is 80.
833    // At first, upsample 240 to 960 (960 is 240 * 320 / 80)
834    // Next downsample 960 to 320 (320 is 960 / (240 / 80)).
835    // Thus, upsampling rate is 320 / 80 = 4, downsampling rate is 240 / 80 = 3.
836    let width_gcd = greatest_common_divisor(src_width, dst_width);
837    let height_gcd = greatest_common_divisor(src_height, dst_height);
838    let upsample_x = dst_width / width_gcd;
839    let downsample_x = src_width / width_gcd;
840    let upsample_y = dst_height / height_gcd;
841    let downsample_y = src_height / height_gcd;
842
843    // Upsampling and downsampling are performed in the same loop.
844    // Upsample the image while the size is indivisible by downsampling rate.
845    // Then downsample and clear the buffer.
846    // For example, upsampling rate is 3 and downsampling rate is 4.
847    // After processing 4 pixels, buffer gets 12 pixels (repeats each pixel 3 times).
848    // 12 pixels can be downsampled by 4, so downsampling takes every 4th pixel.
849    // The result contains 3 pixels. That approach is somewhat slower but requires less memory.
850    let img_pixels = &img.raw_pixels;
851    let src_chan = 4;
852
853    // Resample width.
854    let mut resampled_width = Vec::<u8>::new();
855    for row in 0..src_height {
856        // Upsample pixels and put them to temporary buffer.
857        let mut upsampled_width = Vec::<u8>::new();
858        for col in 0..src_width {
859            for _i in 0..upsample_x {
860                for chan in 0..src_chan {
861                    let src_idx = (row * src_width * src_chan) + col * src_chan + chan;
862                    upsampled_width.push(img_pixels[src_idx]);
863                }
864            }
865
866            // When the temporary buffer can be downsampled, downsample and clear it.
867            let upsampled_pix_count = upsampled_width.len() / src_chan;
868            if (upsampled_pix_count % downsample_x) == 0 {
869                for i in 0..upsampled_pix_count / downsample_x {
870                    for chan in 0..src_chan {
871                        let src_idx = (i * downsample_x) * src_chan + chan;
872                        resampled_width.push(upsampled_width[src_idx]);
873                    }
874                }
875                upsampled_width.clear();
876            }
877        }
878    }
879
880    // Resample height.
881    let mut upsampled_height = Vec::<u8>::new();
882    for row in 0..src_height {
883        // Upsample rows and put them to temporary buffer.
884        for _i in 0..upsample_y {
885            let mut row_copy = copy_row(&resampled_width, row, dst_width * src_chan);
886            upsampled_height.append(&mut row_copy);
887        }
888
889        // When the temporary buffer can be downsampled, downsample and clear it.
890        let upsampled_rows_count = upsampled_height.len() / src_chan / dst_width;
891        if (upsampled_rows_count % downsample_y) == 0 {
892            for i in 0..upsampled_rows_count / downsample_y {
893                let mut row_copy =
894                    copy_row(&upsampled_height, i * downsample_y, dst_width * src_chan);
895                pix_buf.append(&mut row_copy);
896            }
897            upsampled_height.clear();
898        }
899    }
900
901    PhotonImage::new(pix_buf, dst_width as u32, dst_height as u32)
902}
903
904/// Compression
905///
906/// # Arguments
907/// * `img` - A PhotonImage.
908/// * `quality` - The Quality of the returned PhotonImage.
909///
910/// # Example
911///
912/// ```no_run
913/// use photon_rs::native::open_image;
914/// use photon_rs::transform::compress;
915///
916/// let image = open_image("img.jpg").expect("File should open");
917///
918/// let compressed_image = compress(&image, 50);
919/// ```
920/// Adds a constant to a select R, G, or B channel's value.
921pub fn compress(img: &PhotonImage, quality: u8) -> PhotonImage {
922    let bytes = img.get_bytes_jpeg(quality);
923
924    PhotonImage::new_from_byteslice(bytes)
925}