Skip to content

Commit 9206536

Browse files
authored
Added watermark Rust example (#8)
* Added watermark Rust example * Addressed review comments * cleanup
1 parent 56c5cbb commit 9206536

File tree

4 files changed

+255
-0
lines changed

4 files changed

+255
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
target = "wasm32-wasi"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[package]
2+
name = "watermark"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
fastedge = { path = "../../" }
8+
urlencoding = "2.1.2"
9+
url = "2.3.1"
10+
image = "0.24.5"
11+
rusty-s3 = "0.5.0"
12+
anyhow = "1.0.72"
13+
14+
[lib]
15+
crate-type = ["cdylib"]
16+
17+
[workspace]
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// this example reads file from S3 storage, which must be confgiured for the app like this:
2+
// "env": {
3+
// "ACCESS_KEY": "<access_key>",
4+
// "BASE_HOSTNAME": "<base_hostname>, e.g. cloud.gcore.lu",
5+
// "BUCKET": "<bucket>",
6+
// "REGION": "<region>",
7+
// "SECRET_KEY": "<secret_key>"
8+
// }
9+
// then apply watermark from file "sample.png", which is embedded during compilation process
10+
// and return resulting image as PNG.
11+
// if file from S3 cannot be recognised as valid image, it is passed to caller as is
12+
13+
const DEFAULT_OPACITY: f32 = 1.0; // to use non-default opacity, specify OPACITY in 0-1.0 range in app env
14+
15+
use std::{
16+
time::Duration,
17+
env,
18+
io::Cursor
19+
};
20+
use fastedge::{
21+
body::Body,
22+
http::{Error, header, Request, Response, StatusCode, Method}
23+
};
24+
use url::Url;
25+
use image::*;
26+
use rusty_s3::{Bucket, Credentials, S3Action, UrlStyle};
27+
28+
#[fastedge::http]
29+
fn main(req: Request<Body>) -> Result<Response<Body>, Error> {
30+
// embed watermark file - file must be present during compilation
31+
let wm_buf = include_bytes!("sample.png");
32+
33+
// Filter request methods
34+
match req.method() {
35+
// Allow only GET and HEAD requests.
36+
&Method::GET | &Method::HEAD => (),
37+
38+
// Deny anything else.
39+
_ => {
40+
return Response::builder()
41+
.status(StatusCode::METHOD_NOT_ALLOWED)
42+
.header(header::ALLOW, "GET, HEAD")
43+
.body(Body::from("This method is not allowed\n"));
44+
}
45+
};
46+
47+
// get filename from URL with has format <scheme>://<host>/<filename>
48+
let filename = req
49+
.uri()
50+
.path()
51+
.trim_start_matches("/");
52+
if filename.is_empty() {
53+
return Response::builder()
54+
.status(StatusCode::BAD_REQUEST)
55+
.body(Body::from("Malformed request - filename expected\n"));
56+
}
57+
58+
// construct S3 signed URL
59+
let (signed_url, host) = match sign_s3(filename) {
60+
Err(_) => {
61+
return Response::builder()
62+
.status(StatusCode::INTERNAL_SERVER_ERROR)
63+
.body(Body::from("App misconfigured\n"))
64+
}
65+
Ok((u, h)) => (u, h),
66+
};
67+
68+
/* Actual request to S3 */
69+
let s3_req = Request::builder()
70+
.method(Method::GET)
71+
.uri(signed_url.as_str())
72+
.header("Host", host)
73+
.body(Body::empty())
74+
.expect("error building the request");
75+
let rsp = match fastedge::send_request(s3_req) {
76+
Err(_) => {
77+
return Response::builder()
78+
.status(StatusCode::INTERNAL_SERVER_ERROR)
79+
.body(Body::empty())
80+
}
81+
Ok(r) => r,
82+
};
83+
84+
// if response is not 200, just forward it to the caller
85+
let (parts, body) = rsp.into_parts();
86+
if parts.status != StatusCode::OK {
87+
return Ok(Response::from_parts(parts, body))
88+
// if you don't want to expose S3 error to the caller, just use
89+
// return Response::builder()
90+
// .status(StatusCode::INTERNAL_SERVER_ERROR)
91+
// .body(Body::empty())
92+
}
93+
94+
// load response as image
95+
let buf = body.as_bytes();
96+
let out_format = match guess_format(buf) {
97+
Ok(f) => f,
98+
Err(_e) =>
99+
// response body is not a valid image, just return it to the caller without changes
100+
return Ok(Response::from_parts(parts, body))
101+
};
102+
let img = match load_from_memory(buf) {
103+
Ok(i) => i,
104+
Err(_e) =>
105+
// response body is not a valid image, just return it to the caller without changes
106+
return Ok(Response::from_parts(parts, body))
107+
};
108+
109+
// load watermark as image
110+
let wm_img = match load_from_memory(wm_buf.as_slice()) {
111+
Ok(i) => i,
112+
Err(_e) =>
113+
// should never happen
114+
return Response::builder()
115+
.status(StatusCode::INTERNAL_SERVER_ERROR)
116+
.body(Body::from("Invalid watermark format\n"))
117+
};
118+
119+
// get opacity from env
120+
let opacity = match env::var("OPACITY").ok() {
121+
None => DEFAULT_OPACITY,
122+
Some(l) => match l.parse::<f32>() {
123+
Err(_) => return Response::builder() // opacity is not a number
124+
.status(StatusCode::INTERNAL_SERVER_ERROR)
125+
.body(Body::from("Invalid opacity value\n")),
126+
Ok(v) if v < 0.0 || v > 1.0 => // opacity is not in 0-1.0 range
127+
return Response::builder()
128+
.status(StatusCode::INTERNAL_SERVER_ERROR)
129+
.body(Body::from("Invalid opacity value\n")),
130+
Ok(v) => v
131+
},
132+
};
133+
134+
let result = watermark(
135+
&img,
136+
&wm_img,
137+
0, // X offset for watermark placement
138+
0, // Y offset for watermark placement
139+
opacity);
140+
141+
// convert resulting image to original format
142+
let mut out = Vec::new();
143+
let mut c = Cursor::new(&mut out);
144+
let _ = result.write_to(&mut c, out_format);
145+
146+
Response::builder()
147+
.status(StatusCode::OK)
148+
.header(
149+
header::CONTENT_TYPE,
150+
out_format.to_mime_type(),
151+
)
152+
.body(Body::from(out))
153+
}
154+
155+
// Apply watermark using alpha blending
156+
fn watermark(
157+
img: &DynamicImage,
158+
wm: &DynamicImage,
159+
offset_x: u32,
160+
offset_y: u32,
161+
opacity: f32) -> DynamicImage {
162+
163+
let opacity = match opacity {
164+
o if o > 1.0 => 1.0,
165+
o if o < 0.0 => 0.0,
166+
_ => opacity
167+
};
168+
169+
let img_width = img.width();
170+
let img_height = img.height();
171+
172+
let mut wm_width = wm.width();
173+
let mut wm_height = wm.height();
174+
175+
if offset_x + wm_width > img_width {
176+
wm_width = img_width - offset_x;
177+
}
178+
179+
if offset_y + wm_height > img_height {
180+
wm_height = img_height - offset_y;
181+
}
182+
183+
let mut canvas = img.clone();
184+
185+
for y in 0..wm_height {
186+
for x in 0..wm_width {
187+
let img_x = x + offset_x;
188+
let img_y = y + offset_y;
189+
190+
let mut img_pixel = img.get_pixel(img_x, img_y);
191+
let wm_pixel = wm.get_pixel(x, y);
192+
193+
let img_alpha = img_pixel.0[3] as f32 / 255.0;
194+
let img_red = img_pixel.0[0] as f32 * img_alpha;
195+
let img_green = img_pixel.0[1] as f32 * img_alpha;
196+
let img_blue = img_pixel.0[2] as f32 * img_alpha;
197+
198+
let wm_alpha = wm_pixel.0[3] as f32 / 255.0 * opacity;
199+
let wm_red = wm_pixel.0[0] as f32 * wm_alpha;
200+
let wm_green = wm_pixel.0[1] as f32 * wm_alpha;
201+
let wm_blue = wm_pixel.0[2] as f32 * wm_alpha;
202+
203+
img_pixel.0[0] = (wm_red + (1.0 - wm_alpha) * img_red) as u8;
204+
img_pixel.0[1] = (wm_green + (1.0 - wm_alpha) * img_green) as u8;
205+
img_pixel.0[2] = (wm_blue + (1.0 - wm_alpha) * img_blue) as u8;
206+
img_pixel.0[3] = 255;
207+
208+
canvas.put_pixel(img_x, img_y, img_pixel);
209+
}
210+
}
211+
212+
canvas
213+
}
214+
215+
// Calculate S3 signature
216+
fn sign_s3(fname: &str) -> anyhow::Result<(Url, String)> {
217+
/* read S3 access params from env */
218+
let access_key = env::var("ACCESS_KEY")?;
219+
let secret_key = env::var("SECRET_KEY")?;
220+
let region = env::var("REGION")?;
221+
let base_hostname = env::var("BASE_HOSTNAME")?;
222+
let bucket = env::var("BUCKET")?;
223+
let scheme = env::var("SCHEME").unwrap_or_else(|_| "http".to_string());
224+
225+
/* set S3 request params */
226+
let host = region.clone() + "." + base_hostname.as_str();
227+
let upload_url = scheme + "://" + host.as_str();
228+
let parsed_url = upload_url.parse()?;
229+
let bucket = Bucket::new(parsed_url, UrlStyle::Path, bucket, region)?;
230+
231+
let creds = Credentials::new(access_key, secret_key);
232+
let action = bucket.get_object(Some(&creds), fname);
233+
let signed_url = action.sign(Duration::from_secs(60 * 60));
234+
235+
Ok((signed_url, host))
236+
}
Loading

0 commit comments

Comments
 (0)