Skip to main content

vexy_vsvg_wasm/
lib.rs

1// this_file: crates/vexy-vsvg-wasm/src/lib.rs
2
3//! WebAssembly bindings for Vexy Vsvg.
4//!
5//! Provides two API tiers:
6//! - **Enhanced**: Full-featured API with metrics, streaming, and error details
7//! - **Minimal**: Smallest possible bundle (~50KB gzipped) with basic optimization only
8//!
9//! # Usage (JavaScript/TypeScript)
10//!
11//! ```javascript
12//! import init, { optimize } from './vexy_vsvg_wasm.js';
13//!
14//! await init();
15//!
16//! const config = new JsConfig();
17//! config.multipass = true;
18//! config.pretty = true;
19//!
20//! const result = optimize(svgString, config);
21//! console.log(`Reduced by ${result.sizeReduction}%`);
22//! console.log(result.data);
23//! ```
24//!
25//! # Bundle Size
26//!
27//! - Enhanced: ~200KB gzipped (includes all 52 plugins)
28//! - Minimal: ~50KB gzipped (6 most impactful plugins only)
29
30#[cfg(feature = "size-optimization")]
31pub mod minimal;
32
33#[cfg(target_arch = "wasm32")]
34pub mod enhanced;
35
36#[cfg(target_arch = "wasm32")]
37pub mod wasm_impl {
38    use serde::{Deserialize, Serialize};
39    use vexy_vsvg_core::{optimize_with_config, Config};
40    use wasm_bindgen::prelude::*;
41
42    /// Use wee_alloc for smaller WASM bundles.
43    ///
44    /// wee_alloc sacrifices speed for size, reducing bundle by ~10-20KB.
45    #[cfg(feature = "wasm")]
46    #[global_allocator]
47    static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
48
49    /// Initialize WASM runtime in debug mode.
50    ///
51    /// Sets up panic hooks for readable error messages in browser console.
52    /// Only included in debug builds to save bundle space.
53    #[cfg(debug_assertions)]
54    #[wasm_bindgen(start)]
55    pub fn init() {
56        #[cfg(feature = "console_error_panic_hook")]
57        console_error_panic_hook::set_once();
58    }
59
60    /// Initialize WASM runtime in release mode (no-op).
61    ///
62    /// Release builds skip initialization to reduce bundle size.
63    #[cfg(not(debug_assertions))]
64    #[wasm_bindgen(start)]
65    pub fn init() {
66        // No-op in release to save space
67    }
68
69    /// Configuration object exposed to JavaScript.
70    ///
71    /// Simplified version of the core `Config` struct with only the most
72    /// commonly used options.
73    #[wasm_bindgen]
74    #[derive(Serialize, Deserialize)]
75    pub struct JsConfig {
76        /// Path to the SVG file (optional, used for error messages)
77        path: Option<String>,
78        /// Multipass optimization (run until no changes)
79        pub multipass: bool,
80        /// Pretty print output
81        pub pretty: bool,
82        /// Indentation size for pretty printing
83        pub indent: u8,
84        /// Plugins configuration as JSON string
85        plugins_json: String,
86    }
87
88    #[wasm_bindgen]
89    impl JsConfig {
90        #[wasm_bindgen(getter)]
91        pub fn path(&self) -> Option<String> {
92            self.path.clone()
93        }
94
95        #[wasm_bindgen(setter)]
96        pub fn set_path(&mut self, path: Option<String>) {
97            self.path = path;
98        }
99    }
100
101    #[wasm_bindgen]
102    impl JsConfig {
103        /// Create a new configuration with default values
104        #[wasm_bindgen(constructor)]
105        pub fn new() -> Self {
106            Self {
107                path: None,
108                multipass: false,
109                pretty: false,
110                indent: 2,
111                plugins_json: "{}".to_string(),
112            }
113        }
114
115        /// Set the plugins configuration from a JSON string
116        #[wasm_bindgen(js_name = setPlugins)]
117        pub fn set_plugins(&mut self, plugins_json: &str) {
118            self.plugins_json = plugins_json.to_string();
119        }
120
121        /// Get the plugins configuration as a JSON string
122        #[wasm_bindgen(js_name = getPlugins)]
123        pub fn get_plugins(&self) -> String {
124            self.plugins_json.clone()
125        }
126    }
127
128    impl Default for JsConfig {
129        fn default() -> Self {
130            Self::new()
131        }
132    }
133
134    /// Optimization result returned to JavaScript.
135    ///
136    /// Contains the optimized SVG data, size metrics, and any error messages.
137    #[wasm_bindgen]
138    pub struct JsOptimizationResult {
139        data: String,
140        error: Option<String>,
141        pub original_size: usize,
142        pub optimized_size: usize,
143    }
144
145    #[wasm_bindgen]
146    impl JsOptimizationResult {
147        /// Get the optimized SVG data
148        #[wasm_bindgen(getter)]
149        pub fn data(&self) -> String {
150            self.data.clone()
151        }
152
153        /// Get the error message (if any)
154        #[wasm_bindgen(getter)]
155        pub fn error(&self) -> Option<String> {
156            self.error.clone()
157        }
158
159        /// Get the original size in bytes
160        #[wasm_bindgen(getter, js_name = originalSize)]
161        pub fn original_size(&self) -> usize {
162            self.original_size
163        }
164
165        /// Get the optimized size in bytes
166        #[wasm_bindgen(getter, js_name = optimizedSize)]
167        pub fn optimized_size(&self) -> usize {
168            self.optimized_size
169        }
170
171        /// Get the compression ratio (0.0 to 1.0)
172        #[wasm_bindgen(getter, js_name = compressionRatio)]
173        pub fn compression_ratio(&self) -> f64 {
174            if self.original_size == 0 {
175                1.0
176            } else {
177                self.optimized_size as f64 / self.original_size as f64
178            }
179        }
180
181        /// Get the size reduction percentage
182        #[wasm_bindgen(getter, js_name = sizeReduction)]
183        pub fn size_reduction(&self) -> f64 {
184            (1.0 - self.compression_ratio()) * 100.0
185        }
186    }
187
188    /// Optimize an SVG string.
189    ///
190    /// The primary function for JavaScript users. Takes an SVG string and optional
191    /// configuration, returns an optimization result or error.
192    ///
193    /// # Example (JavaScript)
194    ///
195    /// ```js
196    /// const result = optimize('<svg><rect/></svg>', null);
197    /// console.log(result.data);
198    /// ```
199    #[wasm_bindgen]
200    pub fn optimize(svg: &str, config: Option<JsConfig>) -> Result<JsOptimizationResult, JsError> {
201        let config = config.unwrap_or_default();
202        let original_size = svg.len();
203
204        // Convert JavaScript config to native config
205        let mut native_config = Config::with_default_preset();
206        native_config.multipass = config.multipass;
207        native_config.js2svg.pretty = config.pretty;
208        native_config.js2svg.indent = config.indent.to_string();
209
210        // Parse plugins configuration
211        if !config.plugins_json.is_empty() && config.plugins_json != "{}" {
212            match serde_json::from_str::<serde_json::Value>(&config.plugins_json) {
213                Ok(plugins) => {
214                    if let Some(plugins_obj) = plugins.as_object() {
215                        for (name, enabled) in plugins_obj {
216                            if let Some(enabled) = enabled.as_bool() {
217                                native_config.set_plugin_enabled(name, enabled);
218                            }
219                        }
220                    }
221                }
222                Err(e) => {
223                    return Ok(JsOptimizationResult {
224                        data: svg.to_string(),
225                        error: Some(format!("Invalid plugins configuration: {}", e)),
226                        original_size,
227                        optimized_size: original_size,
228                    });
229                }
230            }
231        }
232
233        // Optimize the SVG
234        match optimize_with_config(svg, native_config) {
235            Ok(result) => Ok(JsOptimizationResult {
236                data: result.data.clone(),
237                error: None,
238                original_size,
239                optimized_size: result.data.len(),
240            }),
241            Err(e) => Ok(JsOptimizationResult {
242                data: svg.to_string(),
243                error: Some(e.to_string()),
244                original_size,
245                optimized_size: original_size,
246            }),
247        }
248    }
249
250    /// Optimize an SVG using default settings.
251    ///
252    /// Convenience function that uses the default plugin preset.
253    #[wasm_bindgen(js_name = optimizeDefault)]
254    pub fn optimize_default(svg: &str) -> Result<JsOptimizationResult, JsError> {
255        optimize(svg, None)
256    }
257
258    /// Get the library version string.
259    #[wasm_bindgen(js_name = getVersion)]
260    pub fn get_version() -> String {
261        env!("CARGO_PKG_VERSION").to_string()
262    }
263
264    /// Get a JSON array of available plugin names.
265    #[wasm_bindgen(js_name = getPlugins)]
266    pub fn get_plugins() -> Result<String, JsError> {
267        let config = Config::with_default_preset();
268        let plugins: Vec<String> = config
269            .plugins
270            .iter()
271            .map(|p| p.name().to_string())
272            .collect();
273        serde_json::to_string(&plugins).map_err(|e| JsError::new(&e.to_string()))
274    }
275
276    /// Get the default plugin configuration as JSON.
277    #[wasm_bindgen(js_name = getDefaultPreset)]
278    pub fn get_default_preset() -> Result<String, JsError> {
279        let config = Config::with_default_preset();
280        serde_json::to_string(&config).map_err(|e| JsError::new(&e.to_string()))
281    }
282
283    /// Optimize large SVG files in chunks (placeholder).
284    ///
285    /// Future optimization for processing huge SVGs without hitting memory limits.
286    /// Currently delegates to `optimize()` as chunking is not yet implemented.
287    /// The `chunk_size` parameter is reserved for future use.
288    #[wasm_bindgen(js_name = optimizeChunked)]
289    pub fn optimize_chunked(
290        svg: &str,
291        config: Option<JsConfig>,
292        chunk_size: usize,
293    ) -> Result<JsOptimizationResult, JsError> {
294        // For now, just call the regular optimize function
295        // In the future, this could be implemented to process large SVGs in chunks
296        let _ = chunk_size; // Suppress unused warning
297        optimize(svg, config)
298    }
299
300    /// Hint to JavaScript runtime that memory can be freed.
301    ///
302    /// This is advisory only; WASM memory is managed automatically by the runtime.
303    /// Call after processing large SVGs to encourage garbage collection. May have
304    /// no effect depending on the browser's GC strategy.
305    #[wasm_bindgen(js_name = freeMemory)]
306    pub fn free_memory() {
307        // This is a hint for JavaScript garbage collection
308        // In Rust/WASM, memory is managed automatically
309    }
310
311    #[cfg(test)]
312    mod tests {
313        use super::*;
314
315        #[test]
316        fn test_js_config_default() {
317            let config = JsConfig::new();
318            assert_eq!(config.multipass, false);
319            assert_eq!(config.pretty, false);
320            assert_eq!(config.indent, 2);
321            assert_eq!(config.plugins_json, "{}");
322        }
323
324        #[test]
325        fn test_js_config_plugins() {
326            let mut config = JsConfig::new();
327            config.set_plugins("{\"removeComments\": true}");
328            assert_eq!(config.get_plugins(), "{\"removeComments\": true}");
329        }
330
331        #[test]
332        fn test_optimization_result() {
333            let result = JsOptimizationResult {
334                data: "optimized".to_string(),
335                error: None,
336                original_size: 100,
337                optimized_size: 50,
338            };
339
340            assert_eq!(result.compression_ratio(), 0.5);
341            assert_eq!(result.size_reduction(), 50.0);
342        }
343    }
344}
345
346// Re-export the WASM functionality when targeting WASM
347#[cfg(target_arch = "wasm32")]
348#[cfg(target_arch = "wasm32")]
349pub use enhanced::*;
350
351// Provide stub implementations for non-WASM targets
352#[cfg(not(target_arch = "wasm32"))]
353pub mod stub {
354    //! Stub implementations for non-WASM targets.
355    //!
356    //! Allows this crate to compile on native platforms (for testing and
357    //! benchmarking) even though the actual WASM functionality is disabled.
358}