Skip to main content

vexy_vsvg/optimizer/
mod.rs

1// this_file: crates/vexy-vsvg/src/optimizer/mod.rs
2
3//! Core optimization engine
4//!
5//! This module provides the main optimization functionality that orchestrates
6//! parsing, plugin application, and output generation.
7
8#[cfg(feature = "parallel")]
9pub mod parallel;
10
11pub mod memory;
12
13use std::sync::Arc;
14
15use serde::{Deserialize, Serialize};
16
17use crate::error::VexyError;
18use crate::parser::config::{Config, DataUriFormat};
19use crate::parser::Parser;
20use crate::plugin_registry::PluginRegistry;
21
22/// Optimization result type
23pub type OptimizeResult<T> = Result<T, VexyError>;
24
25/// Options for the optimize function
26pub struct OptimizeOptions {
27    /// Configuration to use
28    pub config: Config,
29    /// Plugin registry (if None, uses default)
30    pub registry: Option<PluginRegistry>,
31    /// Optional memory budget
32    pub memory_budget: Option<std::sync::Arc<memory::MemoryBudget>>,
33    /// Enable parallel plugin execution (opt-in, default: false)
34    pub parallel: bool,
35}
36
37/// Result of an optimization operation
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct OptimizationResult {
40    /// Optimized SVG data
41    pub data: String,
42    /// Optimization information
43    pub info: OptimizationInfo,
44    /// Error message (if any)
45    pub error: Option<String>,
46    /// Whether modern parser was used
47    pub modern: bool,
48}
49
50/// Information about the optimization process
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct OptimizationInfo {
53    /// Original size in bytes
54    pub original_size: usize,
55    /// Optimized size in bytes
56    pub optimized_size: usize,
57    /// Compression ratio (0.0 to 1.0)
58    pub compression_ratio: f64,
59    /// Number of plugins applied
60    pub plugins_applied: usize,
61    /// Number of optimization passes
62    pub passes: usize,
63}
64
65impl OptimizeOptions {
66    /// Create new options with the given config
67    pub fn new(config: Config) -> Self {
68        Self {
69            config,
70            registry: None,
71            memory_budget: None,
72            parallel: false,
73        }
74    }
75
76    /// Set the plugin registry
77    pub fn with_registry(mut self, registry: PluginRegistry) -> Self {
78        self.registry = Some(registry);
79        self
80    }
81
82    /// Set the memory budget
83    pub fn with_memory_budget(mut self, budget: std::sync::Arc<memory::MemoryBudget>) -> Self {
84        self.memory_budget = Some(budget);
85        self
86    }
87
88    /// Enable or disable parallel plugin execution
89    pub fn with_parallel(mut self, parallel: bool) -> Self {
90        self.parallel = parallel;
91        self
92    }
93}
94
95impl Default for OptimizeOptions {
96    fn default() -> Self {
97        Self::new(Config::default())
98    }
99}
100
101impl OptimizationInfo {
102    /// Create new optimization info
103    pub fn new(
104        original_size: usize,
105        optimized_size: usize,
106        plugins_applied: usize,
107        passes: usize,
108    ) -> Self {
109        let compression_ratio = if original_size > 0 {
110            1.0 - (optimized_size as f64 / original_size as f64)
111        } else {
112            0.0
113        };
114
115        Self {
116            original_size,
117            optimized_size,
118            compression_ratio,
119            plugins_applied,
120            passes,
121        }
122    }
123
124    /// Get the size reduction in bytes
125    pub fn size_reduction(&self) -> i64 {
126        self.original_size as i64 - self.optimized_size as i64
127    }
128
129    /// Get the compression percentage (0-100)
130    pub fn compression_percentage(&self) -> f64 {
131        self.compression_ratio * 100.0
132    }
133}
134
135/// Main optimization function
136///
137/// This is the primary entry point for SVG optimization, equivalent to SVGO's `optimize` function.
138/// It parses the SVG, applies a series of plugins, and then stringifies the result.
139///
140/// # Arguments
141///
142/// * `input` - The SVG string to optimize
143/// * `options` - Configuration options including plugins and output settings
144///
145/// # Returns
146///
147/// An `OptimizationResult` containing the optimized SVG and metadata about the optimization
148///
149/// # Examples
150///
151/// ```rust
152/// use vexy_vsvg::{optimize, Config, OptimizeOptions};
153///
154/// let svg = r#"<svg><rect width="100" height="100"/></svg>"#;
155/// let options = OptimizeOptions::new(Config::default());
156/// let result = optimize(svg, options).unwrap();
157/// println!("Optimized SVG: {}", result.data);
158/// println!("Compression: {:.1}%", result.info.compression_percentage());
159/// ```
160pub fn optimize(input: &str, options: OptimizeOptions) -> Result<OptimizationResult, VexyError> {
161    let original_size = input.len();
162    #[cfg(feature = "parallel")]
163    let parallel_enabled = options.parallel;
164    let config = options.config;
165    let registry = options.registry.unwrap_or_default();
166
167    let mut document = Parser::parse_svg_string(input)?;
168
169    if let Some(ref budget) = options.memory_budget {
170        document.memory_budget = Some(Arc::clone(budget));
171    }
172
173    #[cfg(feature = "parallel")]
174    {
175        if parallel_enabled {
176            let parallel_config = parallel::ParallelConfig {
177                num_threads: config.parallel.unwrap_or(0),
178                ..parallel::ParallelConfig::default()
179            };
180            parallel::configure_thread_pool(&parallel_config);
181            crate::debug_log!(
182                "Configured parallel processing with {} threads",
183                parallel_config.num_threads
184            );
185        }
186    }
187
188    let mut passes = 0;
189    let mut plugins_applied = 0;
190    let mut previous_hash: u64 = 0;
191    let registry_plugins = &config.plugins;
192
193    // Build stringify config once outside the loop
194    let stringify_config = crate::stringifier::StringifyConfig {
195        pretty: config.js2svg.pretty,
196        indent: config.js2svg.indent.clone(),
197        newlines: config.js2svg.pretty,
198        quote_attrs: true,
199        self_close: config.js2svg.use_short_tags,
200        initial_capacity: 4096,
201    };
202
203    // Reusable output buffer — avoids re-allocating multi-MB strings each pass
204    let mut current_output = String::new();
205
206    loop {
207        passes += 1;
208
209        #[cfg(feature = "parallel")]
210        {
211            if parallel_enabled {
212                let num_threads = config.parallel.unwrap_or(0);
213                registry
214                    .apply_plugins_parallel(&mut document, registry_plugins, num_threads)
215                    .map_err(VexyError::from)?;
216            } else {
217                registry
218                    .apply_plugins(&mut document, registry_plugins)
219                    .map_err(VexyError::from)?;
220            }
221        }
222        #[cfg(not(feature = "parallel"))]
223        {
224            registry
225                .apply_plugins(&mut document, registry_plugins)
226                .map_err(VexyError::from)?;
227        }
228        plugins_applied += config.plugins.len();
229
230        crate::stringifier::stringify_into_buffer(
231            &document,
232            &stringify_config,
233            &mut current_output,
234        )?;
235
236        // Hash comparison instead of full multi-MB string equality
237        use std::hash::{Hash, Hasher};
238        let mut hasher = std::collections::hash_map::DefaultHasher::new();
239        current_output.hash(&mut hasher);
240        let current_hash = hasher.finish();
241
242        if !config.multipass || current_hash == previous_hash || passes >= 10 {
243            let mut final_output = if let Some(ref format) = config.datauri {
244                apply_datauri_encoding(&current_output, format)
245            } else {
246                current_output
247            };
248            if config.js2svg.final_newline && !final_output.ends_with('\n') {
249                final_output.push('\n');
250            }
251            let optimized_size = final_output.len();
252            let info =
253                OptimizationInfo::new(original_size, optimized_size, plugins_applied, passes);
254            return Ok(OptimizationResult {
255                data: final_output,
256                info,
257                error: None,
258                modern: true,
259            });
260        }
261        // Re-parse from stringified output for next pass.
262        // This matches SVGO's behavior: each pass starts from a clean parse of the
263        // previous pass's output, avoiding stale AST state from in-place mutations.
264        document = Parser::parse_svg_string(&current_output)?;
265        if let Some(ref budget) = options.memory_budget {
266            document.memory_budget = Some(Arc::clone(budget));
267        }
268        previous_hash = current_hash;
269    }
270}
271
272/// Convenience function with default options
273///
274/// This is a convenience function that optimizes an SVG using the default
275/// configuration with all standard plugins enabled.
276///
277/// # Arguments
278///
279/// * `input` - The SVG string to optimize
280///
281/// # Returns
282///
283/// An `OptimizationResult` containing the optimized SVG
284///
285/// # Examples
286///
287/// ```rust
288/// use vexy_vsvg::optimize_default;
289///
290/// let svg = r#"<svg><rect width="100" height="100"/></svg>"#;
291/// let result = optimize_default(svg).unwrap();
292/// assert!(!result.data.is_empty());
293/// ```
294pub fn optimize_default(input: &str) -> Result<OptimizationResult, VexyError> {
295    optimize(input, OptimizeOptions::new(Config::with_default_preset()))
296}
297
298/// Optimize with a custom configuration
299///
300/// This function allows you to specify a custom configuration for the optimization process.
301/// Use this when you need fine-grained control over which plugins to run and their parameters.
302///
303/// # Arguments
304///
305/// * `input` - The SVG string to optimize
306/// * `config` - Custom configuration specifying plugins and their settings
307///
308/// # Returns
309///
310/// An `OptimizationResult` containing the optimized SVG
311///
312/// # Examples
313///
314/// ```rust
315/// use vexy_vsvg::{optimize_with_config, Config, PluginConfig};
316///
317/// let mut config = Config::default();
318/// config.multipass = true;
319/// config.plugins = vec![
320///     PluginConfig::Name("removeComments".to_string()),
321///     PluginConfig::Name("removeDoctype".to_string()),
322/// ];
323///
324/// let svg = r#"<!DOCTYPE svg><!-- test --><svg><rect/></svg>"#;
325/// let result = optimize_with_config(svg, config).unwrap();
326/// ```
327pub fn optimize_with_config(input: &str, config: Config) -> Result<OptimizationResult, VexyError> {
328    optimize(input, OptimizeOptions::new(config))
329}
330
331/// Apply data URI encoding to SVG content
332#[cfg(feature = "data-uri")]
333pub fn apply_datauri_encoding(svg: &str, format: &DataUriFormat) -> String {
334    match format {
335        DataUriFormat::Base64 => {
336            use base64::engine::general_purpose::STANDARD;
337            use base64::Engine as _;
338            let encoded = STANDARD.encode(svg.as_bytes());
339            format!("data:image/svg+xml;base64,{}", encoded)
340        }
341        DataUriFormat::Enc => {
342            use urlencoding::encode;
343            let encoded = encode(svg);
344            format!("data:image/svg+xml,{encoded}")
345        }
346        DataUriFormat::Unenc => {
347            format!("data:image/svg+xml,{svg}")
348        }
349    }
350}
351
352#[cfg(not(feature = "data-uri"))]
353pub fn apply_datauri_encoding(svg: &str, _format: &DataUriFormat) -> String {
354    // Fallback implementation without encoding
355    format!("data:image/svg+xml,{svg}")
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::ast::{Document, Node};
362    use crate::parser::config::Config;
363    use crate::plugin_registry::PluginRegistry;
364    use crate::Plugin;
365
366    // Simple test plugin to remove comments
367    struct RemoveCommentsPlugin;
368
369    impl Plugin for RemoveCommentsPlugin {
370        fn name(&self) -> &'static str {
371            "removeComments"
372        }
373
374        fn description(&self) -> &'static str {
375            "Remove comments"
376        }
377
378        fn apply(&self, document: &mut Document) -> anyhow::Result<()> {
379            // Remove comments from root children
380            document
381                .root
382                .children
383                .retain(|node| !matches!(node, Node::Comment(_)));
384            // Remove comments from prologue
385            document
386                .prologue
387                .retain(|node| !matches!(node, Node::Comment(_)));
388            // Remove comments from epilogue
389            document
390                .epilogue
391                .retain(|node| !matches!(node, Node::Comment(_)));
392            Ok(())
393        }
394    }
395
396    #[test]
397    fn test_optimize_simple_svg() {
398        let svg = r#"<svg width="100" height="100">
399            <!-- This is a comment -->
400            <rect x="10" y="10" width="50" height="50"/>
401        </svg>"#;
402
403        // Create config with removeComments plugin
404        let mut config = Config::new();
405        config.plugins = vec![crate::parser::config::PluginConfig::Name(
406            "removeComments".to_string(),
407        )];
408
409        // Create a registry with our test plugin
410        let mut registry = PluginRegistry::new();
411        registry.register("removeComments", || RemoveCommentsPlugin);
412
413        let result = optimize(svg, OptimizeOptions::new(config).with_registry(registry)).unwrap();
414
415        assert!(!result.data.is_empty());
416        assert!(result.info.original_size > 0);
417        assert!(result.info.optimized_size > 0);
418        assert!(result.modern);
419        // Check that comment was removed
420        assert!(!result.data.contains("<!--"));
421    }
422
423    #[test]
424    fn test_optimize_with_config() {
425        let svg = r#"<svg><rect/></svg>"#;
426        let mut config = Config::with_default_preset();
427        config.js2svg.pretty = true;
428        config.js2svg.indent = "    ".to_string();
429
430        let result = optimize_with_config(svg, config).unwrap();
431        assert!(!result.data.is_empty());
432    }
433
434    #[test]
435    fn test_optimization_info() {
436        let info = OptimizationInfo::new(1000, 800, 5, 2);
437
438        assert_eq!(info.original_size, 1000);
439        assert_eq!(info.optimized_size, 800);
440        assert_eq!(info.size_reduction(), 200);
441        assert!((info.compression_percentage() - 20.0).abs() < 0.01);
442        assert_eq!(info.plugins_applied, 5);
443        assert_eq!(info.passes, 2);
444    }
445
446    #[test]
447    #[cfg(feature = "data-uri")]
448    fn test_datauri_encoding() {
449        use base64::engine::general_purpose::STANDARD;
450        use base64::Engine as _;
451
452        use crate::parser::config::DataUriFormat;
453
454        let svg = "<svg><circle r=\"5\"/></svg>";
455
456        // Test Base64 encoding
457        let base64_result = apply_datauri_encoding(svg, &DataUriFormat::Base64);
458        assert!(base64_result.starts_with("data:image/svg+xml;base64,"));
459        let expected_base64 = STANDARD.encode(svg.as_bytes());
460        assert_eq!(
461            base64_result,
462            format!("data:image/svg+xml;base64,{}", expected_base64)
463        );
464
465        // Test URL encoding
466        let enc_result = apply_datauri_encoding(svg, &DataUriFormat::Enc);
467        assert!(enc_result.starts_with("data:image/svg+xml,"));
468        assert!(enc_result.contains("%3Csvg%3E%3Ccircle%20r%3D%225%22%2F%3E%3C%2Fsvg%3E"));
469
470        // Test unencoded
471        let unenc_result = apply_datauri_encoding(svg, &DataUriFormat::Unenc);
472        assert_eq!(unenc_result, format!("data:image/svg+xml,{}", svg));
473    }
474}