-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlib.rs
More file actions
129 lines (111 loc) · 4.16 KB
/
lib.rs
File metadata and controls
129 lines (111 loc) · 4.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
use base64::Engine;
use image::{ImageReader, ImageFormat};
use openruntimes::{Context, Response};
use ravif::{Encoder, Img};
use serde_json::json;
use std::collections::HashMap;
use std::io::Cursor;
pub fn main(mut context: Context) -> Response {
if context.req.method != "POST" {
return context.res.text(
"POST a base64-encoded image in the `image` field, set `format` to webp, avif, or jpeg, and `width` for the target width.",
None,
None,
);
}
let body = context.req.body_json().unwrap_or(serde_json::Value::Null);
let image_b64 = match body.get("image").and_then(|v| v.as_str()) {
Some(s) => s,
None => return error_response(&context, 400, "Missing `image` field"),
};
let format = body
.get("format")
.and_then(|v| v.as_str())
.unwrap_or("webp")
.to_lowercase();
let width = body
.get("width")
.and_then(|v| v.as_u64())
.unwrap_or(800) as u32;
if width == 0 {
return error_response(&context, 400, "`width` must be greater than 0");
}
let bytes = match base64::engine::general_purpose::STANDARD.decode(image_b64) {
Ok(b) => b,
Err(e) => return error_response(&context, 400, &format!("Invalid base64: {}", e)),
};
let original_size = bytes.len();
let img = match ImageReader::new(Cursor::new(&bytes))
.with_guessed_format()
.map(|r| r.decode())
{
Ok(Ok(img)) => img,
Ok(Err(e)) => return error_response(&context, 400, &format!("Decode failed: {}", e)),
Err(e) => return error_response(&context, 400, &format!("Format detection failed: {}", e)),
};
let (orig_w, orig_h) = (img.width(), img.height());
let height = (orig_h as f32 * (width as f32 / orig_w as f32)).round() as u32;
let resized = img.resize_exact(width, height, image::imageops::FilterType::Lanczos3);
let encoded = match format.as_str() {
"webp" => encode_webp(&resized),
"jpeg" | "jpg" => encode_with_image(&resized, ImageFormat::Jpeg),
"avif" => encode_avif(&resized),
other => return error_response(&context, 400, &format!("Unsupported format `{}`", other)),
};
let encoded = match encoded {
Ok(b) => b,
Err(e) => return error_response(&context, 500, &e),
};
context.log(format!(
"Optimized {}x{} ({} bytes) -> {}x{} {} ({} bytes)",
orig_w, orig_h, original_size, width, height, format, encoded.len()
));
let mut headers = HashMap::new();
headers.insert("content-type".to_string(), "application/json".to_string());
context.res.json(
json!({
"format": format,
"width": width,
"height": height,
"originalBytes": original_size,
"optimizedBytes": encoded.len(),
"image": base64::engine::general_purpose::STANDARD.encode(&encoded),
}),
None,
Some(headers),
)
}
fn encode_with_image(img: &image::DynamicImage, format: ImageFormat) -> Result<Vec<u8>, String> {
let mut buf = Cursor::new(Vec::new());
img.write_to(&mut buf, format)
.map_err(|e| format!("Encode {:?} failed: {}", format, e))?;
Ok(buf.into_inner())
}
fn encode_webp(img: &image::DynamicImage) -> Result<Vec<u8>, String> {
let encoder = webp::Encoder::from_image(img)
.map_err(|e| format!("WebP encoder init failed: {}", e))?;
Ok(encoder.encode(75.0).to_vec())
}
fn encode_avif(img: &image::DynamicImage) -> Result<Vec<u8>, String> {
let rgba = img.to_rgba8();
let (w, h) = rgba.dimensions();
let pixels: Vec<rgb::RGBA<u8>> = rgba
.pixels()
.map(|p| rgb::RGBA::new(p[0], p[1], p[2], p[3]))
.collect();
let img = Img::new(pixels.as_slice(), w as usize, h as usize);
Encoder::new()
.with_quality(70.0)
.with_speed(8)
.encode_rgba(img)
.map(|res| res.avif_file)
.map_err(|e| format!("AVIF encode failed: {}", e))
}
fn error_response(context: &Context, code: u16, message: &str) -> Response {
context.error(message);
context.res.json(
json!({ "ok": false, "error": message }),
Some(code),
None,
)
}