Skip to main content

bwipp/
wasm.rs

1//! WebAssembly / JS bindings.
2//!
3//! Compile with:
4//!
5//! ```sh
6//! cargo build --target wasm32-unknown-unknown --no-default-features --features wasm
7//! ```
8//!
9//! The exported surface is intentionally tiny: three functions that let a
10//! JavaScript host pick a symbology, encode some data, and receive an SVG
11//! string or PNG bytes. Options are passed as a flat JS object
12//! `{ scale: 4, eclevel: "M", ... }`; recognised numeric fields override
13//! the equivalent fields on [`crate::Options`], and any remaining
14//! key/value pairs become string-valued `extras`.
15
16#![allow(missing_docs)]
17
18use wasm_bindgen::prelude::*;
19
20use crate::{render_png as r_png, render_svg as r_svg, Options, Symbology};
21
22/// Wasm-bindgen start hook: installs a panic handler that forwards to
23/// `console.error` so JS users see readable stack traces.
24#[wasm_bindgen(start)]
25pub fn __bwipp_rs_init() {
26    // Forward panics to console.error so JS users get readable stack traces.
27    console_error_panic_hook::set_once();
28}
29
30#[wasm_bindgen]
31extern "C" {
32    #[wasm_bindgen(typescript_type = "Record<string, string | number | boolean> | undefined")]
33    pub type JsOpts;
34}
35
36/// Render a barcode to an SVG string.
37#[wasm_bindgen(js_name = renderSvg)]
38pub fn render_svg(symbology_id: &str, data: &str, opts: Option<JsOpts>) -> Result<String, JsError> {
39    let sym = resolve_symbology(symbology_id)?;
40    let options = options_from_js(opts)?;
41    r_svg(sym, data, &options).map_err(|e| JsError::new(&e.to_string()))
42}
43
44/// Render a barcode to PNG bytes (returned as a `Uint8Array`).
45#[wasm_bindgen(js_name = renderPng)]
46pub fn render_png(
47    symbology_id: &str,
48    data: &str,
49    opts: Option<JsOpts>,
50) -> Result<Vec<u8>, JsError> {
51    let sym = resolve_symbology(symbology_id)?;
52    let options = options_from_js(opts)?;
53    r_png(sym, data, &options).map_err(|e| JsError::new(&e.to_string()))
54}
55
56/// Return every supported symbology id (matches [`Symbology::all`]).
57#[wasm_bindgen(js_name = listSymbologies)]
58pub fn list_symbologies() -> Vec<js_sys::JsString> {
59    Symbology::all()
60        .iter()
61        .map(|s| js_sys::JsString::from(s.id()))
62        .collect()
63}
64
65/// Return `[id, displayName, category, defaultData]` quadruples for every
66/// supported symbology. Useful for populating a grouped UI dropdown and
67/// prefilling the input field with realistic sample data.
68#[wasm_bindgen(js_name = listSymbologyDetails)]
69pub fn list_symbology_details() -> Vec<js_sys::Array> {
70    Symbology::all()
71        .iter()
72        .map(|s| {
73            let arr = js_sys::Array::new();
74            arr.push(&js_sys::JsString::from(s.id()).into());
75            arr.push(&js_sys::JsString::from(s.display_name()).into());
76            arr.push(&js_sys::JsString::from(s.category()).into());
77            arr.push(&js_sys::JsString::from(s.default_data()).into());
78            arr
79        })
80        .collect()
81}
82
83/// Return the bundled sample payload for a symbology id, or an empty
84/// string for unknown ids. Lets the demo prefill the input field without
85/// duplicating the table on the JS side.
86#[wasm_bindgen(js_name = defaultData)]
87pub fn default_data(symbology_id: &str) -> String {
88    match Symbology::from_id(symbology_id) {
89        Some(s) => s.default_data().to_string(),
90        None => String::new(),
91    }
92}
93
94/// Return the symbology-specific default option overrides as an object
95/// (e.g. `{ columns: "8" }` for `codablockf`). Empty object for symbologies
96/// without any defaults. The demo merges this into the options it passes
97/// back to [`render_svg`] so stacked symbologies render at a sensible size.
98#[wasm_bindgen(js_name = defaultExtras)]
99pub fn default_extras(symbology_id: &str) -> js_sys::Object {
100    let obj = js_sys::Object::new();
101    if let Some(s) = Symbology::from_id(symbology_id) {
102        for (k, v) in s.default_extras() {
103            let _ = js_sys::Reflect::set(
104                &obj,
105                &js_sys::JsString::from(*k).into(),
106                &js_sys::JsString::from(*v).into(),
107            );
108        }
109    }
110    obj
111}
112
113fn resolve_symbology(id: &str) -> Result<Symbology, JsError> {
114    Symbology::from_id(id).ok_or_else(|| JsError::new(&format!("unknown symbology id: {id}")))
115}
116
117fn options_from_js(opts: Option<JsOpts>) -> Result<Options, JsError> {
118    let Some(opts) = opts else {
119        return Ok(Options::default());
120    };
121    let js_val: JsValue = opts.into();
122    if js_val.is_null() || js_val.is_undefined() {
123        return Ok(Options::default());
124    }
125    let obj: js_sys::Object = js_val
126        .dyn_into()
127        .map_err(|_| JsError::new("options must be a plain object"))?;
128
129    let mut out = Options::default();
130    let entries = js_sys::Object::entries(&obj);
131    for i in 0..entries.length() {
132        let pair = js_sys::Array::from(&entries.get(i));
133        let key = pair
134            .get(0)
135            .as_string()
136            .ok_or_else(|| JsError::new("options key must be a string"))?;
137        let val = pair.get(1);
138        apply_option(&mut out, &key, val);
139    }
140    Ok(out)
141}
142
143fn apply_option(out: &mut Options, key: &str, val: JsValue) {
144    match key {
145        "scale" => {
146            if let Some(n) = val.as_f64() {
147                out.scale = n as u32;
148            }
149        }
150        "bar_height" | "barHeight" => {
151            if let Some(n) = val.as_f64() {
152                out.bar_height = n as u32;
153            }
154        }
155        "quiet_zone" | "quietZone" => {
156            if let Some(n) = val.as_f64() {
157                out.quiet_zone = n as u32;
158            }
159        }
160        "include_text" | "includeText" | "includetext" => {
161            if let Some(b) = val.as_bool() {
162                out.include_text = b;
163            } else if let Some(s) = val.as_string() {
164                out.include_text = s == "true";
165            }
166        }
167        _ => {
168            let value = if let Some(s) = val.as_string() {
169                s
170            } else if let Some(b) = val.as_bool() {
171                if b {
172                    "true".to_string()
173                } else {
174                    "false".to_string()
175                }
176            } else if let Some(n) = val.as_f64() {
177                n.to_string()
178            } else {
179                return;
180            };
181            out.extras.push((key.to_string(), value));
182        }
183    }
184}