Skip to content

Commit 2633d70

Browse files
Merge pull request #19 from StanleyMasinde/feat/chaining
feat: add chaining to the image processor
2 parents bf71133 + 561b25b commit 2633d70

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ crate-type = ["cdylib", "rlib"]
2626
image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "webp"] }
2727
wasm-bindgen = "0.2.100"
2828

29+
[profile.release]
30+
lto = true

src/chaining/mod.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
use wasm_bindgen::{JsError, JsValue, prelude::wasm_bindgen};
2+
3+
use crate::color_filters::{
4+
blur::blur, brighten::brighten, contrast::contrast, fast_blur::fast_blur, grayscale::grayscale,
5+
hue_rotate::hue_rotate, invert::invert,
6+
};
7+
use crate::transformation::{
8+
crop::crop, resize::resize, resize_square::resize_square, thumbnail::thumbnail,
9+
};
10+
11+
// This class is here to be used in a builder pattern.
12+
// It allows for a single image to go through multiplce modifications
13+
#[wasm_bindgen]
14+
#[derive(Debug, Default)]
15+
/// Builder-style image processor for JS/Wasm usage.
16+
///
17+
/// ```javascript
18+
/// // Browser usage (after wasm-bindgen or bundler setup)
19+
/// // const { ImageProcessor } = await init();
20+
/// const input = document.querySelector("#file");
21+
/// const outputImg = document.querySelector("#output");
22+
///
23+
/// input.addEventListener("change", async (event) => {
24+
/// const file = event.target.files[0];
25+
/// if (!file) return;
26+
///
27+
/// const inputBytes = new Uint8Array(await file.arrayBuffer());
28+
/// const outputBytes = new ImageProcessor(inputBytes)
29+
/// .resize(512, 512)
30+
/// .grayscale()
31+
/// .contrast(25.0)
32+
/// .process();
33+
///
34+
/// const blob = new Blob([outputBytes], { type: file.type });
35+
/// outputImg.src = URL.createObjectURL(blob);
36+
/// });
37+
/// ```
38+
///
39+
/// ```javascript
40+
/// // Canvas usage (after wasm-bindgen or bundler setup)
41+
/// // const { ImageProcessor } = await init();
42+
/// const input = document.querySelector("#file");
43+
/// const canvas = document.querySelector("#canvas");
44+
/// const ctx = canvas.getContext("2d");
45+
///
46+
/// input.addEventListener("change", async (event) => {
47+
/// const file = event.target.files[0];
48+
/// if (!file) return;
49+
///
50+
/// const inputBytes = new Uint8Array(await file.arrayBuffer());
51+
/// const outputBytes = new ImageProcessor(inputBytes)
52+
/// .resize(512, 512)
53+
/// .blur(2.0)
54+
/// .process();
55+
///
56+
/// const blob = new Blob([outputBytes], { type: file.type });
57+
/// const bitmap = await createImageBitmap(blob);
58+
/// canvas.width = bitmap.width;
59+
/// canvas.height = bitmap.height;
60+
/// ctx.clearRect(0, 0, canvas.width, canvas.height);
61+
/// ctx.drawImage(bitmap, 0, 0);
62+
/// });
63+
/// ```
64+
pub struct ImageProcessor {
65+
image: Vec<u8>,
66+
}
67+
68+
fn to_js_error(context: &str, err: JsValue) -> JsError {
69+
let message = err
70+
.as_string()
71+
.unwrap_or_else(|| "Unknown error".to_string());
72+
JsError::new(&format!("{context}: {message}"))
73+
}
74+
75+
#[wasm_bindgen]
76+
impl ImageProcessor {
77+
/// Create a new processor from raw image bytes.
78+
pub fn new(image: Vec<u8>) -> Self {
79+
Self { image }
80+
}
81+
82+
/// Calling this returns the final image bytes.
83+
pub fn process(self) -> Vec<u8> {
84+
self.image
85+
}
86+
87+
pub fn resize(mut self, width: u32, height: u32) -> Result<Self, JsError> {
88+
let buff = resize(self.image, width, height)
89+
.map_err(|err| to_js_error("Failed to resize the image", err))?;
90+
self.image = buff;
91+
Ok(self)
92+
}
93+
94+
pub fn resize_square(mut self, side: u32) -> Result<Self, JsError> {
95+
let buff = resize_square(self.image, side)
96+
.map_err(|err| to_js_error("Failed to resize the image", err))?;
97+
self.image = buff;
98+
Ok(self)
99+
}
100+
101+
pub fn thumbnail(mut self, width: u32, height: u32) -> Result<Self, JsError> {
102+
let buff = thumbnail(self.image, width, height)
103+
.map_err(|err| to_js_error("Failed to create the thumbnail", err))?;
104+
self.image = buff;
105+
Ok(self)
106+
}
107+
108+
pub fn crop(mut self, x: u32, y: u32, width: u32, height: u32) -> Result<Self, JsError> {
109+
let buff = crop(self.image, x, y, width, height)
110+
.map_err(|err| to_js_error("Failed to crop the image", err))?;
111+
self.image = buff;
112+
Ok(self)
113+
}
114+
115+
pub fn blur(mut self, sigma: f32) -> Result<Self, JsError> {
116+
let buff =
117+
blur(self.image, sigma).map_err(|err| to_js_error("Failed to blur the image", err))?;
118+
self.image = buff;
119+
Ok(self)
120+
}
121+
122+
pub fn fast_blur(mut self, sigma: f32) -> Result<Self, JsError> {
123+
let buff = fast_blur(self.image, sigma)
124+
.map_err(|err| to_js_error("Failed to blur the image", err))?;
125+
self.image = buff;
126+
Ok(self)
127+
}
128+
129+
pub fn brighten(mut self, value: i32) -> Result<Self, JsError> {
130+
let buff = brighten(self.image, value)
131+
.map_err(|err| to_js_error("Failed to adjust brightness", err))?;
132+
self.image = buff;
133+
Ok(self)
134+
}
135+
136+
pub fn contrast(mut self, value: f32) -> Result<Self, JsError> {
137+
let buff = contrast(self.image, value)
138+
.map_err(|err| to_js_error("Failed to adjust contrast", err))?;
139+
self.image = buff;
140+
Ok(self)
141+
}
142+
143+
pub fn grayscale(mut self) -> Result<Self, JsError> {
144+
let buff = grayscale(self.image)
145+
.map_err(|err| to_js_error("Failed to convert to grayscale", err))?;
146+
self.image = buff;
147+
Ok(self)
148+
}
149+
150+
pub fn invert(mut self) -> Result<Self, JsError> {
151+
let buff =
152+
invert(self.image).map_err(|err| to_js_error("Failed to invert the image", err))?;
153+
self.image = buff;
154+
Ok(self)
155+
}
156+
157+
pub fn hue_rotate(mut self, degrees: i32) -> Result<Self, JsError> {
158+
let buff = hue_rotate(self.image, degrees)
159+
.map_err(|err| to_js_error("Failed to hue rotate", err))?;
160+
self.image = buff;
161+
Ok(self)
162+
}
163+
}
164+
165+
#[cfg(test)]
166+
mod tests {
167+
use super::*;
168+
169+
#[test]
170+
fn test_chaining() {
171+
let test_image_data = include_bytes!("../../sample.jpg").to_vec();
172+
let result = ImageProcessor::new(test_image_data)
173+
.resize(512, 512)
174+
.unwrap()
175+
.grayscale()
176+
.unwrap()
177+
.contrast(25.0)
178+
.unwrap()
179+
.process();
180+
181+
let result_image = image::load_from_memory(&result).unwrap();
182+
assert_eq!(result_image.width(), 512);
183+
184+
result_image.save("test-output/chaining.jpg").unwrap();
185+
}
186+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use wasm_bindgen::prelude::wasm_bindgen;
33
pub mod color_filters;
44
pub mod transformation;
55
mod utils;
6+
pub mod chaining;
67

78
#[wasm_bindgen]
89
extern "C" {

0 commit comments

Comments
 (0)