Skip to main content

svgm_core/
config.rs

1use std::collections::HashMap;
2
3use crate::passes::{self, Pass};
4
5/// Optimization preset.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum Preset {
8    /// Zero-risk-to-rendering: removal, normalization, and whitespace passes only.
9    Safe,
10    /// Full optimization (default). All passes enabled.
11    #[default]
12    Default,
13}
14
15/// Configuration for the optimization pipeline.
16#[derive(Debug, Clone, Default)]
17pub struct Config {
18    /// Which preset to use as the base pass set.
19    pub preset: Preset,
20    /// Numeric precision for rounding passes. If `None`, uses the preset default
21    /// (default: 3).
22    pub precision: Option<u32>,
23    /// Per-pass overrides. `true` enables a pass not in the preset, `false` disables one that is.
24    pub pass_overrides: HashMap<String, bool>,
25}
26
27impl Config {
28    /// Returns the effective numeric precision for this configuration.
29    pub fn effective_precision(&self) -> u32 {
30        self.precision.unwrap_or(3)
31    }
32}
33
34// (name, in_safe, in_default) — execution order matters.
35const PASS_CATALOG: &[(&str, bool, bool)] = &[
36    ("removeDoctype", true, true),
37    ("removeProcInst", true, true),
38    ("removeComments", true, true),
39    ("removeDeprecatedAttrs", true, true),
40    ("removeMetadata", true, true),
41    ("removeEditorData", true, true),
42    ("removeDesc", false, true),
43    ("removeEmptyAttrs", true, true),
44    ("removeEmptyText", true, true),
45    ("removeHiddenElems", true, true),
46    ("removeUselessDefs", false, true),
47    ("removeUselessStrokeAndFill", false, true),
48    ("removeEmptyContainers", true, true),
49    ("removeUnusedNamespaces", true, true),
50    ("cleanupAttrs", true, true),
51    ("inlineStyles", false, true),
52    ("minifyStyles", true, true),
53    ("cleanupNumericValues", true, true),
54    ("convertColors", true, true),
55    ("removeUnknownsAndDefaults", true, true),
56    ("removeNonInheritableGroupAttrs", false, true),
57    ("cleanupEnableBackground", true, true),
58    ("convertEllipseToCircle", false, true),
59    ("convertShapeToPath", false, true),
60    ("moveElemsAttrsToGroup", false, true),
61    ("moveGroupAttrsToElems", false, true),
62    ("convertTransform", false, true),
63    ("collapseGroups", false, true),
64    ("cleanupIds", false, true),
65    ("convertPathData", false, true),
66    ("mergePaths", false, true),
67    ("sortAttrs", true, true),
68    ("sortDefsChildren", true, true),
69    ("minifyWhitespace", true, true),
70];
71
72fn is_in_preset(entry: &(&str, bool, bool), preset: Preset) -> bool {
73    match preset {
74        Preset::Safe => entry.1,
75        Preset::Default => entry.2,
76    }
77}
78
79fn create_pass(name: &str, precision: u32) -> Box<dyn Pass> {
80    match name {
81        "removeDoctype" => Box::new(passes::remove_doctype::RemoveDoctype),
82        "removeProcInst" => Box::new(passes::remove_proc_inst::RemoveProcInst),
83        "removeComments" => Box::new(passes::remove_comments::RemoveComments),
84        "removeDeprecatedAttrs" => Box::new(passes::remove_deprecated_attrs::RemoveDeprecatedAttrs),
85        "removeMetadata" => Box::new(passes::remove_metadata::RemoveMetadata),
86        "removeEditorData" => Box::new(passes::remove_editor_data::RemoveEditorData),
87        "removeDesc" => Box::new(passes::remove_desc::RemoveDesc),
88        "removeEmptyAttrs" => Box::new(passes::remove_empty_attrs::RemoveEmptyAttrs),
89        "removeEmptyText" => Box::new(passes::remove_empty_text::RemoveEmptyText),
90        "removeHiddenElems" => Box::new(passes::remove_hidden_elems::RemoveHiddenElems),
91        "removeUselessDefs" => Box::new(passes::remove_useless_defs::RemoveUselessDefs),
92        "removeUselessStrokeAndFill" => {
93            Box::new(passes::remove_useless_stroke_and_fill::RemoveUselessStrokeAndFill)
94        }
95        "removeEmptyContainers" => Box::new(passes::remove_empty_containers::RemoveEmptyContainers),
96        "removeUnusedNamespaces" => {
97            Box::new(passes::remove_unused_namespaces::RemoveUnusedNamespaces)
98        }
99        "cleanupAttrs" => Box::new(passes::cleanup_attrs::CleanupAttrs),
100        "inlineStyles" => Box::new(passes::inline_styles::InlineStyles),
101        "minifyStyles" => Box::new(passes::minify_styles::MinifyStyles),
102        "cleanupNumericValues" => {
103            Box::new(passes::cleanup_numeric_values::CleanupNumericValues { precision })
104        }
105        "convertColors" => Box::new(passes::convert_colors::ConvertColors),
106        "removeUnknownsAndDefaults" => {
107            Box::new(passes::remove_unknowns_and_defaults::RemoveUnknownsAndDefaults)
108        }
109        "removeNonInheritableGroupAttrs" => {
110            Box::new(passes::remove_non_inheritable_group_attrs::RemoveNonInheritableGroupAttrs)
111        }
112        "cleanupEnableBackground" => {
113            Box::new(passes::cleanup_enable_background::CleanupEnableBackground)
114        }
115        "convertEllipseToCircle" => {
116            Box::new(passes::convert_ellipse_to_circle::ConvertEllipseToCircle)
117        }
118        "convertShapeToPath" => {
119            Box::new(passes::convert_shape_to_path::ConvertShapeToPath { precision })
120        }
121        "moveElemsAttrsToGroup" => {
122            Box::new(passes::move_elems_attrs_to_group::MoveElemsAttrsToGroup)
123        }
124        "moveGroupAttrsToElems" => {
125            Box::new(passes::move_group_attrs_to_elems::MoveGroupAttrsToElems)
126        }
127        "convertTransform" => Box::new(passes::convert_transform::ConvertTransform { precision }),
128        "collapseGroups" => Box::new(passes::collapse_groups::CollapseGroups),
129        "cleanupIds" => Box::new(passes::cleanup_ids::CleanupIds),
130        "convertPathData" => Box::new(passes::convert_path_data::ConvertPathData { precision }),
131        "mergePaths" => Box::new(passes::merge_paths::MergePaths),
132        "sortAttrs" => Box::new(passes::sort_attrs::SortAttrs),
133        "sortDefsChildren" => Box::new(passes::sort_defs_children::SortDefsChildren),
134        "minifyWhitespace" => Box::new(passes::minify_whitespace::MinifyWhitespace),
135        _ => panic!("unknown pass: {name}"),
136    }
137}
138
139/// Build the pass list for a given configuration.
140pub fn passes_for_config(config: &Config) -> Vec<Box<dyn Pass>> {
141    let precision = config.effective_precision();
142    let mut result = Vec::new();
143
144    for entry in PASS_CATALOG {
145        let name = entry.0;
146        let enabled = if let Some(&override_val) = config.pass_overrides.get(name) {
147            override_val
148        } else {
149            is_in_preset(entry, config.preset)
150        };
151
152        if enabled {
153            result.push(create_pass(name, precision));
154        }
155    }
156
157    result
158}
159
160/// Returns the list of all known pass names in execution order.
161pub fn all_pass_names() -> Vec<&'static str> {
162    PASS_CATALOG.iter().map(|e| e.0).collect()
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    fn pass_names(config: &Config) -> Vec<&'static str> {
170        passes_for_config(config).iter().map(|p| p.name()).collect()
171    }
172
173    #[test]
174    fn safe_preset_passes() {
175        let config = Config {
176            preset: Preset::Safe,
177            ..Config::default()
178        };
179        let names = pass_names(&config);
180        assert_eq!(
181            names,
182            vec![
183                "removeDoctype",
184                "removeProcInst",
185                "removeComments",
186                "removeDeprecatedAttrs",
187                "removeMetadata",
188                "removeEditorData",
189                "removeEmptyAttrs",
190                "removeEmptyText",
191                "removeHiddenElems",
192                "removeEmptyContainers",
193                "removeUnusedNamespaces",
194                "cleanupAttrs",
195                "minifyStyles",
196                "cleanupNumericValues",
197                "convertColors",
198                "removeUnknownsAndDefaults",
199                "cleanupEnableBackground",
200                "sortAttrs",
201                "sortDefsChildren",
202                "minifyWhitespace",
203            ]
204        );
205        assert_eq!(names.len(), 20);
206    }
207
208    #[test]
209    fn default_preset_passes() {
210        let config = Config::default();
211        assert_eq!(config.preset, Preset::Default);
212        let names = pass_names(&config);
213        assert_eq!(
214            names,
215            vec![
216                "removeDoctype",
217                "removeProcInst",
218                "removeComments",
219                "removeDeprecatedAttrs",
220                "removeMetadata",
221                "removeEditorData",
222                "removeDesc",
223                "removeEmptyAttrs",
224                "removeEmptyText",
225                "removeHiddenElems",
226                "removeUselessDefs",
227                "removeUselessStrokeAndFill",
228                "removeEmptyContainers",
229                "removeUnusedNamespaces",
230                "cleanupAttrs",
231                "inlineStyles",
232                "minifyStyles",
233                "cleanupNumericValues",
234                "convertColors",
235                "removeUnknownsAndDefaults",
236                "removeNonInheritableGroupAttrs",
237                "cleanupEnableBackground",
238                "convertEllipseToCircle",
239                "convertShapeToPath",
240                "moveElemsAttrsToGroup",
241                "moveGroupAttrsToElems",
242                "convertTransform",
243                "collapseGroups",
244                "cleanupIds",
245                "convertPathData",
246                "mergePaths",
247                "sortAttrs",
248                "sortDefsChildren",
249                "minifyWhitespace",
250            ]
251        );
252        assert_eq!(names.len(), 34);
253    }
254
255    #[test]
256    fn override_enables_pass_not_in_preset() {
257        // removeDesc is in Default but not in Safe — enable it via override
258        let config = Config {
259            preset: Preset::Safe,
260            pass_overrides: HashMap::from([("removeDesc".to_string(), true)]),
261            ..Config::default()
262        };
263        let names = pass_names(&config);
264        assert!(names.contains(&"removeDesc"));
265    }
266
267    #[test]
268    fn override_disables_preset_pass() {
269        let config = Config {
270            preset: Preset::Default,
271            pass_overrides: HashMap::from([("collapseGroups".to_string(), false)]),
272            ..Config::default()
273        };
274        let names = pass_names(&config);
275        assert!(!names.contains(&"collapseGroups"));
276    }
277
278    #[test]
279    fn effective_precision_defaults() {
280        assert_eq!(
281            Config {
282                preset: Preset::Safe,
283                ..Config::default()
284            }
285            .effective_precision(),
286            3
287        );
288        assert_eq!(Config::default().effective_precision(), 3);
289    }
290
291    #[test]
292    fn explicit_precision_overrides_default() {
293        let config = Config {
294            precision: Some(4),
295            ..Config::default()
296        };
297        assert_eq!(config.effective_precision(), 4);
298    }
299}