image_optimizer/optimization/
svg_optimizer.rs1use anyhow::{Context, Result};
2use image::DynamicImage;
3use regex::Regex;
4use std::fs;
5use std::path::Path;
6
7use crate::cli::Cli;
8
9pub fn optimize_svg(
35 input_path: &Path,
36 output_path: &Path,
37 _args: &Cli,
38 _resized_img: Option<DynamicImage>,
39) -> Result<()> {
40 let input_content = fs::read_to_string(input_path)
41 .with_context(|| format!("Failed to read SVG file: {}", input_path.display()))?;
42
43 let optimized_content = optimize_svg_content(&input_content)?;
44
45 fs::write(output_path, optimized_content)
46 .with_context(|| format!("Failed to write optimized SVG: {}", output_path.display()))?;
47
48 Ok(())
49}
50
51fn optimize_svg_content(content: &str) -> Result<String> {
53 let mut optimized = content.to_string();
54
55 let comment_regex = Regex::new(r"(?s)<!--.*?-->").context("Failed to compile comment regex")?;
57 optimized = comment_regex.replace_all(&optimized, "").to_string();
58
59 let metadata_regex = Regex::new(r"(?s)<metadata[^>]*>.*?</metadata>")
61 .context("Failed to compile metadata regex")?;
62 optimized = metadata_regex.replace_all(&optimized, "").to_string();
63
64 let inkscape_regex =
66 Regex::new(r#"\s*inkscape:[^=]*="[^"]*""#).context("Failed to compile inkscape regex")?;
67 optimized = inkscape_regex.replace_all(&optimized, "").to_string();
68
69 let adobe_regex =
70 Regex::new(r#"\s*adobe-[^=]*="[^"]*""#).context("Failed to compile adobe regex")?;
71 optimized = adobe_regex.replace_all(&optimized, "").to_string();
72
73 let sodipodi_regex =
75 Regex::new(r#"\s*sodipodi:[^=]*="[^"]*""#).context("Failed to compile sodipodi regex")?;
76 optimized = sodipodi_regex.replace_all(&optimized, "").to_string();
77
78 let whitespace_regex = Regex::new(r"\s+").context("Failed to compile whitespace regex")?;
80 optimized = whitespace_regex.replace_all(&optimized, " ").to_string();
81
82 optimized = optimized
84 .lines()
85 .map(str::trim)
86 .filter(|line| !line.is_empty())
87 .collect::<Vec<&str>>()
88 .join("\n");
89
90 Ok(optimized)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_preserves_essential_svg_elements() {
99 let input = r#"<?xml version="1.0" encoding="UTF-8"?>
100<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
101 <circle cx="50" cy="50" r="40" fill="blue" />
102 <rect x="20" y="20" width="60" height="60" fill="red" opacity="0.5" />
103 <path d="M10 10 L90 90" stroke="black" />
104 <text x="50" y="50">Hello</text>
105 <g transform="rotate(45)">
106 <ellipse cx="30" cy="30" rx="20" ry="15" />
107 </g>
108</svg>"#;
109
110 let result = optimize_svg_content(input).unwrap();
111
112 assert!(result.contains("<svg"));
114 assert!(result.contains("width=\"100\""));
115 assert!(result.contains("height=\"100\""));
116 assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
117 assert!(result.contains("<circle"));
118 assert!(result.contains("cx=\"50\""));
119 assert!(result.contains("cy=\"50\""));
120 assert!(result.contains("r=\"40\""));
121 assert!(result.contains("fill=\"blue\""));
122 assert!(result.contains("<rect"));
123 assert!(result.contains("<path"));
124 assert!(result.contains("d=\"M10 10 L90 90\""));
125 assert!(result.contains("<text"));
126 assert!(result.contains("Hello"));
127 assert!(result.contains("<g"));
128 assert!(result.contains("transform=\"rotate(45)\""));
129 assert!(result.contains("<ellipse"));
130 assert!(result.contains("</svg>"));
131 }
132
133 #[test]
134 fn test_removes_comments_and_metadata() {
135 let input = r#"<?xml version="1.0" encoding="UTF-8"?>
136<!-- This is a comment -->
137<svg xmlns="http://www.w3.org/2000/svg">
138 <metadata>
139 <rdf:RDF>
140 <cc:Work>
141 <dc:title>Test</dc:title>
142 </cc:Work>
143 </rdf:RDF>
144 </metadata>
145 <!-- Another comment -->
146 <circle r="10" />
147</svg>"#;
148
149 let result = optimize_svg_content(input).unwrap();
150
151 assert!(!result.contains("<!-- This is a comment -->"));
153 assert!(!result.contains("<!-- Another comment -->"));
154 assert!(!result.contains("<metadata"));
155 assert!(!result.contains("</metadata>"));
156 assert!(!result.contains("<rdf:RDF"));
157 assert!(!result.contains("<cc:Work"));
158 assert!(!result.contains("<dc:title>"));
159
160 assert!(result.contains("<svg"));
162 assert!(result.contains("<circle"));
163 assert!(result.contains("r=\"10\""));
164 assert!(result.contains("</svg>"));
165 }
166
167 #[test]
168 fn test_removes_editor_specific_attributes() {
169 let input = r#"<svg xmlns="http://www.w3.org/2000/svg"
170 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
171 inkscape:version="1.0"
172 inkscape:current-layer="layer1"
173 sodipodi:docname="test.svg"
174 adobe-illustrator-version="25.0">
175 <circle r="10" inkscape:label="Circle" adobe-blend-mode="normal" />
176</svg>"#;
177
178 let result = optimize_svg_content(input).unwrap();
179
180 assert!(!result.contains("inkscape:version"));
182 assert!(!result.contains("inkscape:current-layer"));
183 assert!(!result.contains("sodipodi:docname"));
184 assert!(!result.contains("adobe-illustrator-version"));
185 assert!(!result.contains("inkscape:label"));
186 assert!(!result.contains("adobe-blend-mode"));
187
188 assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
190 assert!(result.contains("xmlns:inkscape"));
191 assert!(result.contains("<circle"));
192 assert!(result.contains("r=\"10\""));
193 }
194
195 #[test]
196 fn test_preserves_style_and_class_attributes() {
197 let input = r#"<svg xmlns="http://www.w3.org/2000/svg">
198 <style>
199 .red { fill: red; }
200 .blue { fill: blue; }
201 </style>
202 <circle class="red" style="stroke: black; stroke-width: 2" />
203 <rect class="blue" style="opacity: 0.8" />
204</svg>"#;
205
206 let result = optimize_svg_content(input).unwrap();
207
208 assert!(result.contains("<style>"));
210 assert!(result.contains(".red { fill: red; }"));
211 assert!(result.contains(".blue { fill: blue; }"));
212 assert!(result.contains("</style>"));
213 assert!(result.contains("class=\"red\""));
214 assert!(result.contains("class=\"blue\""));
215 assert!(result.contains("style=\"stroke: black; stroke-width: 2\""));
216 assert!(result.contains("style=\"opacity: 0.8\""));
217 }
218
219 #[test]
220 fn test_preserves_definitions_and_uses() {
221 let input = r##"<svg xmlns="http://www.w3.org/2000/svg">
222 <defs>
223 <linearGradient id="grad1">
224 <stop offset="0%" stop-color="red" />
225 <stop offset="100%" stop-color="blue" />
226 </linearGradient>
227 <pattern id="pattern1">
228 <rect width="10" height="10" fill="green" />
229 </pattern>
230 </defs>
231 <rect fill="url(#grad1)" />
232 <circle fill="url(#pattern1)" />
233 <use xlink:href="#someElement" />
234</svg>"##;
235
236 let result = optimize_svg_content(input).unwrap();
237
238 assert!(result.contains("<defs>"));
240 assert!(result.contains("</defs>"));
241 assert!(result.contains("<linearGradient"));
242 assert!(result.contains("id=\"grad1\""));
243 assert!(result.contains("<stop"));
244 assert!(result.contains("stop-color=\"red\""));
245 assert!(result.contains("<pattern"));
246 assert!(result.contains("id=\"pattern1\""));
247 assert!(result.contains("fill=\"url("));
248 assert!(result.contains("grad1)\""));
249 assert!(result.contains("pattern1)\""));
250 assert!(result.contains("<use"));
251 assert!(result.contains("xlink:href=\""));
252 assert!(result.contains("someElement\""));
253 }
254
255 #[test]
256 fn test_preserves_animations() {
257 let input = r#"<svg xmlns="http://www.w3.org/2000/svg">
258 <circle r="10">
259 <animate attributeName="r" values="10;20;10" dur="2s" repeatCount="indefinite" />
260 <animateTransform attributeName="transform" type="rotate"
261 values="0;360" dur="1s" repeatCount="indefinite" />
262 </circle>
263</svg>"#;
264
265 let result = optimize_svg_content(input).unwrap();
266
267 assert!(result.contains("<animate"));
269 assert!(result.contains("attributeName=\"r\""));
270 assert!(result.contains("values=\"10;20;10\""));
271 assert!(result.contains("dur=\"2s\""));
272 assert!(result.contains("repeatCount=\"indefinite\""));
273 assert!(result.contains("<animateTransform"));
274 assert!(result.contains("type=\"rotate\""));
275 assert!(result.contains("values=\"0;360\""));
276 }
277
278 #[test]
279 fn test_normalizes_whitespace_but_preserves_structure() {
280 let input = r#"<svg xmlns="http://www.w3.org/2000/svg" >
281
282
283 <circle cx="50" cy="50" r="40" />
284
285
286 <rect x="10" y="10" />
287
288</svg>"#;
289
290 let result = optimize_svg_content(input).unwrap();
291
292 assert!(result.contains("<svg"));
294 assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
295 assert!(result.contains("<circle"));
296 assert!(result.contains("cx=\"50\""));
297 assert!(result.contains("cy=\"50\""));
298 assert!(result.contains("r=\"40\""));
299 assert!(result.contains("<rect"));
300 assert!(result.contains("x=\"10\""));
301 assert!(result.contains("y=\"10\""));
302 assert!(result.contains("</svg>"));
303
304 assert!(!result.contains(" xmlns"));
306 assert!(!result.contains(" cx"));
307 assert!(!result.contains("\n\n\n"));
308 }
309
310 #[test]
311 fn test_handles_multiline_comments() {
312 let input = r#"<svg xmlns="http://www.w3.org/2000/svg">
313 <!--
314 This is a multiline comment
315 that spans multiple lines
316 and should be removed
317 -->
318 <circle r="10" />
319</svg>"#;
320
321 let result = optimize_svg_content(input).unwrap();
322
323 assert!(!result.contains("This is a multiline comment"));
325 assert!(!result.contains("that spans multiple lines"));
326 assert!(!result.contains("and should be removed"));
327
328 assert!(result.contains("<svg"));
330 assert!(result.contains("<circle"));
331 assert!(result.contains("r=\"10\""));
332 }
333
334 #[test]
335 fn test_preserves_viewbox_and_coordinate_systems() {
336 let input = r#"<svg xmlns="http://www.w3.org/2000/svg"
337 viewBox="0 0 200 200"
338 preserveAspectRatio="xMidYMid meet">
339 <g transform="translate(50, 50) scale(2)">
340 <circle r="10" />
341 </g>
342</svg>"#;
343
344 let result = optimize_svg_content(input).unwrap();
345
346 assert!(result.contains("viewBox=\"0 0 200 200\""));
348 assert!(result.contains("preserveAspectRatio=\"xMidYMid meet\""));
349 assert!(result.contains("transform=\"translate(50, 50) scale(2)\""));
350 }
351
352 #[test]
353 fn test_empty_svg_handled_gracefully() {
354 let input = r#"<svg xmlns="http://www.w3.org/2000/svg"></svg>"#;
355
356 let result = optimize_svg_content(input).unwrap();
357
358 assert!(result.contains("<svg"));
360 assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
361 assert!(result.contains("</svg>"));
362 }
363
364 #[test]
365 fn test_preserves_data_attributes_and_ids() {
366 let input = r#"<svg xmlns="http://www.w3.org/2000/svg" data-name="icon">
367 <circle id="main-circle" data-value="42" class="important" />
368 <rect id="background" data-layer="base" />
369</svg>"#;
370
371 let result = optimize_svg_content(input).unwrap();
372
373 assert!(result.contains("data-name=\"icon\""));
375 assert!(result.contains("id=\"main-circle\""));
376 assert!(result.contains("data-value=\"42\""));
377 assert!(result.contains("class=\"important\""));
378 assert!(result.contains("id=\"background\""));
379 assert!(result.contains("data-layer=\"base\""));
380 }
381}