1#[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
22pub type OptimizeResult<T> = Result<T, VexyError>;
24
25pub struct OptimizeOptions {
27 pub config: Config,
29 pub registry: Option<PluginRegistry>,
31 pub memory_budget: Option<std::sync::Arc<memory::MemoryBudget>>,
33 pub parallel: bool,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct OptimizationResult {
40 pub data: String,
42 pub info: OptimizationInfo,
44 pub error: Option<String>,
46 pub modern: bool,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct OptimizationInfo {
53 pub original_size: usize,
55 pub optimized_size: usize,
57 pub compression_ratio: f64,
59 pub plugins_applied: usize,
61 pub passes: usize,
63}
64
65impl OptimizeOptions {
66 pub fn new(config: Config) -> Self {
68 Self {
69 config,
70 registry: None,
71 memory_budget: None,
72 parallel: false,
73 }
74 }
75
76 pub fn with_registry(mut self, registry: PluginRegistry) -> Self {
78 self.registry = Some(registry);
79 self
80 }
81
82 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 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 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 pub fn size_reduction(&self) -> i64 {
126 self.original_size as i64 - self.optimized_size as i64
127 }
128
129 pub fn compression_percentage(&self) -> f64 {
131 self.compression_ratio * 100.0
132 }
133}
134
135pub 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(¶llel_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 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 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 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(¤t_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 document = Parser::parse_svg_string(¤t_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
272pub fn optimize_default(input: &str) -> Result<OptimizationResult, VexyError> {
295 optimize(input, OptimizeOptions::new(Config::with_default_preset()))
296}
297
298pub fn optimize_with_config(input: &str, config: Config) -> Result<OptimizationResult, VexyError> {
328 optimize(input, OptimizeOptions::new(config))
329}
330
331#[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 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 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 document
381 .root
382 .children
383 .retain(|node| !matches!(node, Node::Comment(_)));
384 document
386 .prologue
387 .retain(|node| !matches!(node, Node::Comment(_)));
388 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 let mut config = Config::new();
405 config.plugins = vec![crate::parser::config::PluginConfig::Name(
406 "removeComments".to_string(),
407 )];
408
409 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 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 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 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 let unenc_result = apply_datauri_encoding(svg, &DataUriFormat::Unenc);
472 assert_eq!(unenc_result, format!("data:image/svg+xml,{}", svg));
473 }
474}