Skip to content

Commit 42bc087

Browse files
committed
Fix: track wasm/mod.rs in git
1 parent e68deb4 commit 42bc087

File tree

2 files changed

+229
-0
lines changed

2 files changed

+229
-0
lines changed

src/wasm/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*
2+
!.gitignore
3+
!mod.rs

src/wasm/mod.rs

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
use std::io::Cursor;
2+
3+
use image::Rgba;
4+
use serde::{Deserialize, Serialize};
5+
use wasm_bindgen::prelude::*;
6+
7+
use crate::{
8+
error::Error,
9+
gif_anim::GifAnim,
10+
input::{Input, Inputs, TextOptions},
11+
render::RenderQuality,
12+
renders::Renders,
13+
repository::Repository,
14+
};
15+
16+
#[derive(Deserialize)]
17+
#[serde(tag = "kind", rename_all = "lowercase")]
18+
enum InputSpec {
19+
Image { layer: u8, bytes: Vec<u8> },
20+
Text {
21+
layer: u8,
22+
text: String,
23+
font_size: Option<f32>,
24+
color: Option<String>,
25+
background: Option<String>,
26+
padding: Option<u32>,
27+
},
28+
}
29+
30+
#[derive(Deserialize, Default)]
31+
struct RenderOptions {
32+
frame: Option<i32>,
33+
auto_zoom: Option<bool>,
34+
width: Option<i32>,
35+
height: Option<i32>,
36+
}
37+
38+
#[derive(Serialize)]
39+
struct TemplateInfo {
40+
width: u32,
41+
height: u32,
42+
frames: u32,
43+
delay: f64,
44+
hold: f64,
45+
palette: Vec<i32>,
46+
}
47+
48+
#[wasm_bindgen]
49+
pub fn template_info(template_zip: &[u8]) -> Result<JsValue, JsValue> {
50+
let repo = Repository::load_from_bytes(template_zip.to_vec()).map_err(to_js_error)?;
51+
let template = repo.template;
52+
let info = TemplateInfo {
53+
width: template.width,
54+
height: template.height,
55+
frames: template.frames,
56+
delay: template.delay,
57+
hold: template.hold,
58+
palette: template.palette,
59+
};
60+
serde_wasm_bindgen::to_value(&info).map_err(|e| JsValue::from_str(&e.to_string()))
61+
}
62+
63+
#[wasm_bindgen]
64+
pub fn render_png(template_zip: &[u8], inputs: JsValue, options: JsValue) -> Result<Vec<u8>, JsValue> {
65+
let input_specs: Vec<InputSpec> =
66+
serde_wasm_bindgen::from_value(inputs).map_err(|e| JsValue::from_str(&e.to_string()))?;
67+
if input_specs.is_empty() {
68+
return Err(JsValue::from_str("At least one input is required."));
69+
}
70+
71+
let options: RenderOptions = if options.is_null() || options.is_undefined() {
72+
RenderOptions::default()
73+
} else {
74+
serde_wasm_bindgen::from_value(options).map_err(|e| JsValue::from_str(&e.to_string()))?
75+
};
76+
77+
let mut inputs = Inputs::new();
78+
for spec in input_specs {
79+
match spec {
80+
InputSpec::Image { layer, bytes } => {
81+
if !is_png(&bytes) {
82+
return Err(JsValue::from_str("Only PNG inputs are supported."));
83+
}
84+
let img = image::load_from_memory(&bytes).map_err(|e| to_js_error(Error::from(e)))?;
85+
inputs.push(Input::from_image(img.to_rgba8(), layer));
86+
}
87+
InputSpec::Text { layer, text, font_size, color, background, padding } => {
88+
let mut options = TextOptions::default();
89+
if let Some(size) = font_size {
90+
options.font_size = size;
91+
}
92+
if let Some(padding) = padding {
93+
options.padding = padding;
94+
}
95+
if let Some(color) = color {
96+
options.color = parse_hex_color(&color)?;
97+
}
98+
if let Some(background) = background {
99+
options.background = parse_hex_color(&background)?;
100+
}
101+
let input = Input::from_text(&text, layer, &options).map_err(to_js_error)?;
102+
inputs.push(input);
103+
}
104+
}
105+
}
106+
107+
let repo = Repository::load_from_bytes(template_zip.to_vec()).map_err(to_js_error)?;
108+
let mut renders = Renders::new(repo, inputs, RenderQuality::Sampled);
109+
110+
if let (Some(w), Some(h)) = (options.width, options.height) {
111+
if w > 0 && h > 0 {
112+
renders.set_size(w, h);
113+
}
114+
}
115+
116+
if options.auto_zoom.unwrap_or(false) {
117+
renders.auto_zoom().map_err(to_js_error)?;
118+
}
119+
120+
let frame = options.frame.unwrap_or(0);
121+
let render = renders.get_render(frame).map_err(to_js_error)?;
122+
encode_png(render.get()).map_err(to_js_error)
123+
}
124+
125+
#[wasm_bindgen]
126+
pub fn render_gif(template_zip: &[u8], inputs: JsValue, options: JsValue) -> Result<Vec<u8>, JsValue> {
127+
let input_specs: Vec<InputSpec> =
128+
serde_wasm_bindgen::from_value(inputs).map_err(|e| JsValue::from_str(&e.to_string()))?;
129+
if input_specs.is_empty() {
130+
return Err(JsValue::from_str("At least one input is required."));
131+
}
132+
133+
let options: RenderOptions = if options.is_null() || options.is_undefined() {
134+
RenderOptions::default()
135+
} else {
136+
serde_wasm_bindgen::from_value(options).map_err(|e| JsValue::from_str(&e.to_string()))?
137+
};
138+
139+
let mut inputs = Inputs::new();
140+
for spec in input_specs {
141+
match spec {
142+
InputSpec::Image { layer, bytes } => {
143+
if !is_png(&bytes) {
144+
return Err(JsValue::from_str("Only PNG inputs are supported."));
145+
}
146+
let img =
147+
image::load_from_memory(&bytes).map_err(|e| to_js_error(Error::from(e)))?;
148+
inputs.push(Input::from_image(img.to_rgba8(), layer));
149+
}
150+
InputSpec::Text { layer, text, font_size, color, background, padding } => {
151+
let mut options = TextOptions::default();
152+
if let Some(size) = font_size {
153+
options.font_size = size;
154+
}
155+
if let Some(padding) = padding {
156+
options.padding = padding;
157+
}
158+
if let Some(color) = color {
159+
options.color = parse_hex_color(&color)?;
160+
}
161+
if let Some(background) = background {
162+
options.background = parse_hex_color(&background)?;
163+
}
164+
let input = Input::from_text(&text, layer, &options).map_err(to_js_error)?;
165+
inputs.push(input);
166+
}
167+
}
168+
}
169+
170+
let repo = Repository::load_from_bytes(template_zip.to_vec()).map_err(to_js_error)?;
171+
let period = repo.get_period();
172+
let hold = repo.get_hold();
173+
let palette_frames = repo.get_palette();
174+
let mut renders = Renders::new(repo, inputs, RenderQuality::Sampled);
175+
176+
if let (Some(w), Some(h)) = (options.width, options.height) {
177+
if w > 0 && h > 0 {
178+
renders.set_size(w, h);
179+
}
180+
}
181+
182+
if options.auto_zoom.unwrap_or(false) {
183+
renders.auto_zoom().map_err(to_js_error)?;
184+
}
185+
186+
let mut gif = GifAnim::new(renders);
187+
gif.set_timing(period, hold);
188+
if !palette_frames.is_empty() {
189+
gif.set_palette_frames(palette_frames);
190+
}
191+
gif.apply().map_err(to_js_error)?;
192+
gif.encode().map_err(to_js_error)
193+
}
194+
195+
fn is_png(bytes: &[u8]) -> bool {
196+
const PNG_SIG: [u8; 8] = [137, 80, 78, 71, 13, 10, 26, 10];
197+
bytes.len() >= PNG_SIG.len() && bytes[..PNG_SIG.len()] == PNG_SIG
198+
}
199+
200+
fn parse_hex_color(value: &str) -> Result<Rgba<u8>, JsValue> {
201+
let hex = value.trim().trim_start_matches('#');
202+
if hex.len() != 6 && hex.len() != 8 {
203+
return Err(JsValue::from_str("Color must be 6 or 8 hex digits."));
204+
}
205+
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| JsValue::from_str("Invalid hex color."))?;
206+
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| JsValue::from_str("Invalid hex color."))?;
207+
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| JsValue::from_str("Invalid hex color."))?;
208+
let a = if hex.len() == 8 {
209+
u8::from_str_radix(&hex[6..8], 16).map_err(|_| JsValue::from_str("Invalid hex color."))?
210+
} else {
211+
255
212+
};
213+
Ok(Rgba([r, g, b, a]))
214+
}
215+
216+
fn encode_png(img: &image::RgbaImage) -> Result<Vec<u8>, Error> {
217+
let mut out = Vec::new();
218+
let mut cursor = Cursor::new(&mut out);
219+
let dyn_img = image::DynamicImage::ImageRgba8(img.clone());
220+
dyn_img.write_to(&mut cursor, image::ImageFormat::Png)?;
221+
Ok(out)
222+
}
223+
224+
fn to_js_error(err: Error) -> JsValue {
225+
JsValue::from_str(&err.to_string())
226+
}

0 commit comments

Comments
 (0)