photon_rs/
lib.rs

1//! A high-performance image processing library, available for use both natively and on the web.
2//!
3//! #### Functions
4//! 96 functions are available, including:
5//! - **Transformations**: Resize, crop, and flip images.
6//! - **Image correction**: Hue rotation, sharpening, brightness adjustment, adjusting saturation, lightening/darkening all within various colour spaces.
7//! - **Convolutions**: Sobel filters, blurs, Laplace effects, edge detection, etc.,
8//! - **Channel manipulation**: Increasing/decreasing RGB channel values, swapping channels, removing channels, etc.
9//! - **Monochrome effects**: Duotoning, greyscaling of various forms, thresholding, sepia, averaging RGB values
10//! - **Colour manipulation**: Work with the image in various colour spaces such as HSL, LCh, and sRGB, and adjust the colours accordingly.
11//! - **Filters**: Over 30 pre-set filters available, incorporating various effects and transformations.
12//! - **Text**: Apply text to imagery in artistic ways, or to watermark, etc.,
13//! - **Watermarking**: Watermark images in multiple formats.
14//! - **Blending**: Blend images together using 10 different techniques, change image backgrounds.
15//!
16//! ## Example
17//! ```no_run
18//! extern crate photon_rs;
19//!
20//! use photon_rs::channels::alter_red_channel;
21//! use photon_rs::native::{open_image};
22//!
23//! fn main() {
24//!     // Open the image (a PhotonImage is returned)
25//!     let mut img = open_image("img.jpg").expect("File should open");
26//!     // Apply a filter to the pixels
27//!     alter_red_channel(&mut img, 25_i16);
28//! }
29//! ```
30//!
31//! This crate contains built-in preset functions, which provide default image processing functionality, as well as functions
32//! that allow for direct, low-level access to channel manipulation.
33//! To view a full demo of filtered imagery, visit the [official website](https://silvia-odwyer.github.io/photon).
34//!
35//! ### WebAssembly Use
36//! To allow for universal communication between the core Rust library and WebAssembly, the functions have been generalised to allow for both native and in-browser use.
37//! [Check out the official guide](https://silvia-odwyer.github.io/photon/guide/) on how to get started with Photon on the web.
38//!
39//! ### Live Demo
40//! View the [official demo of WASM in action](https://silvia-odwyer.github.io/photon).
41
42use base64::{decode, encode};
43use image::DynamicImage::ImageRgba8;
44use image::GenericImage;
45use serde::{Deserialize, Serialize};
46use std::io::Cursor;
47
48#[cfg(feature = "enable_wasm")]
49use wasm_bindgen::prelude::*;
50
51#[cfg(feature = "wasm-bindgen")]
52use wasm_bindgen::Clamped;
53
54#[cfg(feature = "web-sys")]
55use web_sys::{
56    Blob, CanvasRenderingContext2d, HtmlCanvasElement, HtmlImageElement, ImageData,
57};
58
59// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
60// allocator.
61#[cfg(feature = "wee_alloc")]
62#[global_allocator]
63static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
64
65/// Provides the image's height, width, and contains the image's raw pixels.
66/// For use when communicating between JS and WASM, and also natively.
67#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
68#[derive(Serialize, Deserialize, Clone, Debug)]
69pub struct PhotonImage {
70    raw_pixels: Vec<u8>,
71    width: u32,
72    height: u32,
73}
74
75#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
76impl PhotonImage {
77    #[cfg_attr(feature = "enable_wasm", wasm_bindgen(constructor))]
78    /// Create a new PhotonImage from a Vec of u8s, which represent raw pixels.
79    pub fn new(raw_pixels: Vec<u8>, width: u32, height: u32) -> PhotonImage {
80        PhotonImage {
81            raw_pixels,
82            width,
83            height,
84        }
85    }
86
87    /// Create a new PhotonImage from a base64 string.
88    pub fn new_from_base64(base64: &str) -> PhotonImage {
89        base64_to_image(base64)
90    }
91
92    /// Create a new PhotonImage from a byteslice.
93    pub fn new_from_byteslice(vec: Vec<u8>) -> PhotonImage {
94        let slice = vec.as_slice();
95
96        let img = image::load_from_memory(slice).unwrap();
97
98        let raw_pixels = img.to_rgba8().to_vec();
99
100        PhotonImage {
101            raw_pixels,
102            width: img.width(),
103            height: img.height(),
104        }
105    }
106
107    /// Create a new PhotonImage from a Blob/File.
108    #[cfg(feature = "web-sys")]
109    pub fn new_from_blob(blob: Blob) -> PhotonImage {
110        let bytes: js_sys::Uint8Array = js_sys::Uint8Array::new(&blob);
111
112        let vec = bytes.to_vec();
113
114        PhotonImage::new_from_byteslice(vec)
115    }
116
117    /// Create a new PhotonImage from a HTMLImageElement
118    #[cfg(feature = "web-sys")]
119    pub fn new_from_image(image: HtmlImageElement) -> PhotonImage {
120        set_panic_hook();
121
122        let document = web_sys::window().unwrap().document().unwrap();
123
124        let canvas = document
125            .create_element("canvas")
126            .unwrap()
127            .dyn_into::<web_sys::HtmlCanvasElement>()
128            .unwrap();
129
130        canvas.set_width(image.width());
131        canvas.set_height(image.height());
132
133        let context = canvas
134            .get_context("2d")
135            .unwrap()
136            .unwrap()
137            .dyn_into::<CanvasRenderingContext2d>()
138            .unwrap();
139
140        context
141            .draw_image_with_html_image_element(&image, 0.0, 0.0)
142            .unwrap();
143
144        open_image(canvas, context)
145    }
146
147    // pub fn new_from_buffer(buffer: &Buffer, width: u32, height: u32) -> PhotonImage {
148    //     // Convert a Node.js Buffer into a Vec<u8>
149    //     let raw_pixels: Vec<u8> = Uint8Array::new_with_byte_offset_and_length(
150    //         &buffer.buffer(),
151    //         buffer.byte_offset(),
152    //         buffer.length(),
153    //     ).to_vec();
154
155    //     PhotonImage {
156    //         raw_pixels,
157    //         width,
158    //         height,
159    //     }
160    // }
161
162    /// Get the width of the PhotonImage.
163    pub fn get_width(&self) -> u32 {
164        self.width
165    }
166
167    /// Get the PhotonImage's pixels as a Vec of u8s.
168    pub fn get_raw_pixels(&self) -> Vec<u8> {
169        self.raw_pixels.clone()
170    }
171
172    /// Get the height of the PhotonImage.
173    pub fn get_height(&self) -> u32 {
174        self.height
175    }
176
177    /// Convert the PhotonImage to base64.
178    pub fn get_base64(&self) -> String {
179        let mut img = helpers::dyn_image_from_raw(self);
180        img = ImageRgba8(img.to_rgba8());
181
182        let mut buffer = vec![];
183        img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png)
184            .unwrap();
185        let base64 = encode(&buffer);
186
187        let res_base64 = format!("data:image/png;base64,{}", base64.replace("\r\n", ""));
188
189        res_base64
190    }
191
192    /// Convert the PhotonImage to raw bytes. Returns PNG.
193    pub fn get_bytes(&self) -> Vec<u8> {
194        let mut img = helpers::dyn_image_from_raw(self);
195        img = ImageRgba8(img.to_rgba8());
196        let mut buffer = vec![];
197        img.write_to(&mut Cursor::new(&mut buffer), image::ImageOutputFormat::Png)
198            .unwrap();
199        buffer
200    }
201
202    /// Convert the PhotonImage to raw bytes. Returns a JPEG.
203    pub fn get_bytes_jpeg(&self, quality: u8) -> Vec<u8> {
204        let mut img = helpers::dyn_image_from_raw(self);
205        img = ImageRgba8(img.to_rgba8());
206        let mut buffer = vec![];
207        let out_format = image::ImageOutputFormat::Jpeg(quality);
208        img.write_to(&mut Cursor::new(&mut buffer), out_format)
209            .unwrap();
210        buffer
211    }
212
213    /// Convert the PhotonImage to raw bytes. Returns a WEBP.
214    pub fn get_bytes_webp(&self) -> Vec<u8> {
215        let mut img = helpers::dyn_image_from_raw(self);
216        img = ImageRgba8(img.to_rgba8());
217        let mut buffer = vec![];
218        let out_format = image::ImageOutputFormat::WebP;
219        img.write_to(&mut Cursor::new(&mut buffer), out_format)
220            .unwrap();
221        buffer
222    }
223
224    /// Convert the PhotonImage's raw pixels to JS-compatible ImageData.
225    #[cfg(all(feature = "web-sys", feature = "wasm-bindgen"))]
226    #[allow(clippy::unnecessary_mut_passed)]
227    pub fn get_image_data(&mut self) -> ImageData {
228        ImageData::new_with_u8_clamped_array_and_sh(
229            Clamped(&mut self.raw_pixels),
230            self.width,
231            self.height,
232        )
233        .unwrap()
234    }
235
236    /// Convert ImageData to raw pixels, and update the PhotonImage's raw pixels to this.
237    #[cfg(feature = "web-sys")]
238    pub fn set_imgdata(&mut self, img_data: ImageData) {
239        let width = img_data.width();
240        let height = img_data.height();
241        let raw_pixels = to_raw_pixels(img_data);
242        self.width = width;
243        self.height = height;
244        self.raw_pixels = raw_pixels;
245    }
246
247    /// Calculates estimated filesize and returns number of bytes
248    pub fn get_estimated_filesize(&self) -> u64 {
249        let base64_data = self.get_base64();
250        let padding_count = if base64_data.ends_with("==") {
251            2
252        } else if base64_data.ends_with('=') {
253            1
254        } else {
255            0
256        };
257
258        // Size of original string(in bytes) = ceil(6n/8) – padding
259        ((base64_data.len() as f64) * 0.75).ceil() as u64 - padding_count
260    }
261}
262
263/// Create a new PhotonImage from a raw Vec of u8s representing raw image pixels.
264#[cfg(feature = "web-sys")]
265impl From<ImageData> for PhotonImage {
266    fn from(imgdata: ImageData) -> Self {
267        let width = imgdata.width();
268        let height = imgdata.height();
269        let raw_pixels = to_raw_pixels(imgdata);
270        PhotonImage {
271            raw_pixels,
272            width,
273            height,
274        }
275    }
276}
277
278/// RGB color type.
279#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
280#[derive(Serialize, Deserialize, Debug, Clone)]
281pub struct Rgb {
282    r: u8,
283    g: u8,
284    b: u8,
285}
286
287#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
288impl Rgb {
289    #[cfg_attr(feature = "enable_wasm", wasm_bindgen(constructor))]
290    /// Create a new RGB struct.
291    pub fn new(r: u8, g: u8, b: u8) -> Rgb {
292        Rgb { r, g, b }
293    }
294
295    /// Set the Red value.
296    pub fn set_red(&mut self, r: u8) {
297        self.r = r;
298    }
299
300    /// Get the Green value.
301    pub fn set_green(&mut self, g: u8) {
302        self.g = g;
303    }
304
305    /// Set the Blue value.
306    pub fn set_blue(&mut self, b: u8) {
307        self.b = b;
308    }
309
310    /// Get the Red value.
311    pub fn get_red(&self) -> u8 {
312        self.r
313    }
314
315    /// Get the Green value.
316    pub fn get_green(&self) -> u8 {
317        self.g
318    }
319
320    /// Get the Blue value.
321    pub fn get_blue(&self) -> u8 {
322        self.b
323    }
324}
325
326impl From<Vec<u8>> for Rgb {
327    fn from(vec: Vec<u8>) -> Self {
328        if vec.len() != 3 {
329            panic!("Vec length must be equal to 3.")
330        }
331        Rgb::new(vec[0], vec[1], vec[2])
332    }
333}
334
335/// RGBA color type.
336#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
337#[derive(Serialize, Deserialize, Debug)]
338pub struct Rgba {
339    r: u8,
340    g: u8,
341    b: u8,
342    a: u8,
343}
344
345#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
346impl Rgba {
347    #[cfg_attr(feature = "enable_wasm", wasm_bindgen(constructor))]
348    /// Create a new RGBA struct.
349    pub fn new(r: u8, g: u8, b: u8, a: u8) -> Rgba {
350        Rgba { r, g, b, a }
351    }
352
353    /// Set the Red value.
354    pub fn set_red(&mut self, r: u8) {
355        self.r = r;
356    }
357
358    /// Get the Green value.
359    pub fn set_green(&mut self, g: u8) {
360        self.g = g;
361    }
362
363    /// Set the Blue value.
364    pub fn set_blue(&mut self, b: u8) {
365        self.b = b;
366    }
367
368    /// Set the alpha value.
369    pub fn set_alpha(&mut self, a: u8) {
370        self.a = a;
371    }
372
373    /// Get the Red value.
374    pub fn get_red(&self) -> u8 {
375        self.r
376    }
377
378    /// Get the Green value.
379    pub fn get_green(&self) -> u8 {
380        self.g
381    }
382
383    /// Get the Blue value.
384    pub fn get_blue(&self) -> u8 {
385        self.b
386    }
387
388    /// Get the alpha value for this color.
389    pub fn get_alpha(&self) -> u8 {
390        self.a
391    }
392}
393
394impl From<Vec<u8>> for Rgba {
395    fn from(vec: Vec<u8>) -> Self {
396        if vec.len() != 4 {
397            panic!("Vec length must be equal to 4.")
398        }
399        Rgba::new(vec[0], vec[1], vec[2], vec[3])
400    }
401}
402
403///! [temp] Check if WASM is supported.
404#[cfg(feature = "enable_wasm")]
405#[wasm_bindgen]
406pub fn run() -> Result<(), JsValue> {
407    set_panic_hook();
408
409    let window = web_sys::window().expect("No Window found, should have a Window");
410    let document = window
411        .document()
412        .expect("No Document found, should have a Document");
413
414    let p: web_sys::Node = document.create_element("p")?.into();
415    p.set_text_content(Some("You're successfully running WASM!"));
416
417    let body = document
418        .body()
419        .expect("ERR: No body found, should have a body");
420    let body: &web_sys::Node = body.as_ref();
421    body.append_child(&p)?;
422    Ok(())
423}
424
425/// Get the ImageData from a 2D canvas context
426#[cfg(feature = "web-sys")]
427#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
428pub fn get_image_data(
429    canvas: &HtmlCanvasElement,
430    ctx: &CanvasRenderingContext2d,
431) -> ImageData {
432    set_panic_hook();
433    let width = canvas.width();
434    let height = canvas.height();
435
436    // let data: ImageData = ctx.get_image_data(0.0, 0.0, 100.0, 100.0).unwrap();
437    let data = ctx
438        .get_image_data(0.0, 0.0, width as f64, height as f64)
439        .unwrap();
440    let _vec_data = data.data().to_vec();
441    data
442}
443
444/// Place a PhotonImage onto a 2D canvas.
445#[cfg(all(feature = "web-sys", feature = "wasm-bindgen"))]
446#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
447#[allow(non_snake_case)]
448#[allow(clippy::unnecessary_mut_passed)]
449pub fn putImageData(
450    canvas: HtmlCanvasElement,
451    ctx: CanvasRenderingContext2d,
452    new_image: PhotonImage,
453) {
454    // Convert the raw pixels back to an ImageData object.
455    let mut raw_pixels = new_image.raw_pixels;
456    let new_img_data = ImageData::new_with_u8_clamped_array_and_sh(
457        Clamped(&mut raw_pixels),
458        canvas.width(),
459        canvas.height(),
460    );
461
462    // Place the new imagedata onto the canvas
463    ctx.put_image_data(&new_img_data.unwrap(), 0.0, 0.0)
464        .expect("Should put image data on Canvas");
465}
466
467/// Convert a HTML5 Canvas Element to a PhotonImage.
468///
469/// This converts the ImageData found in the canvas context to a PhotonImage,
470/// which can then have effects or filters applied to it.
471#[cfg(feature = "web-sys")]
472#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
473#[no_mangle]
474pub fn open_image(
475    canvas: HtmlCanvasElement,
476    ctx: CanvasRenderingContext2d,
477) -> PhotonImage {
478    let imgdata = get_image_data(&canvas, &ctx);
479    let raw_pixels = to_raw_pixels(imgdata);
480    PhotonImage {
481        raw_pixels,
482        width: canvas.width(),
483        height: canvas.height(),
484    }
485}
486
487/// Convert ImageData to a raw pixel vec of u8s.
488#[cfg(feature = "web-sys")]
489#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
490pub fn to_raw_pixels(imgdata: ImageData) -> Vec<u8> {
491    imgdata.data().to_vec()
492}
493
494/// Convert a base64 string to a PhotonImage.
495#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
496pub fn base64_to_image(base64: &str) -> PhotonImage {
497    let base64_to_vec: Vec<u8> = base64_to_vec(base64);
498
499    let slice = base64_to_vec.as_slice();
500
501    let mut img = image::load_from_memory(slice).unwrap();
502    img = ImageRgba8(img.to_rgba8());
503
504    let width = img.width();
505    let height = img.height();
506
507    let raw_pixels = img.into_bytes();
508
509    PhotonImage {
510        raw_pixels,
511        width,
512        height,
513    }
514}
515
516/// Convert a base64 string to a Vec of u8s.
517#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
518pub fn base64_to_vec(base64: &str) -> Vec<u8> {
519    decode(base64).unwrap()
520}
521
522/// Convert a PhotonImage to JS-compatible ImageData.
523#[cfg(all(feature = "web-sys", feature = "wasm-bindgen"))]
524#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
525#[allow(clippy::unnecessary_mut_passed)]
526pub fn to_image_data(photon_image: PhotonImage) -> ImageData {
527    let mut raw_pixels = photon_image.raw_pixels;
528    let width = photon_image.width;
529    let height = photon_image.height;
530    ImageData::new_with_u8_clamped_array_and_sh(Clamped(&mut raw_pixels), width, height)
531        .unwrap()
532}
533
534#[cfg(not(target_os = "wasi"))]
535fn set_panic_hook() {
536    // When the `console_error_panic_hook` feature is enabled, we can call the
537    // `set_panic_hook` function to get better error messages if we ever panic.
538    #[cfg(feature = "console_error_panic_hook")]
539    console_error_panic_hook::set_once();
540}
541
542pub mod channels;
543pub mod colour_spaces;
544pub mod conv;
545pub mod effects;
546pub mod filters;
547pub mod helpers;
548mod iter;
549pub mod monochrome;
550pub mod multiple;
551pub mod native;
552pub mod noise;
553mod tests;
554pub mod text;
555pub mod transform;