use anyhow::{Context, Result};
use image::DynamicImage;
use regex::Regex;
use std::fs;
use std::path::Path;
use crate::cli::Cli;
pub fn optimize_svg(
input_path: &Path,
output_path: &Path,
_args: &Cli,
_resized_img: Option<DynamicImage>,
) -> Result<()> {
let input_content = fs::read_to_string(input_path)
.with_context(|| format!("Failed to read SVG file: {}", input_path.display()))?;
let optimized_content = optimize_svg_content(&input_content)?;
fs::write(output_path, optimized_content)
.with_context(|| format!("Failed to write optimized SVG: {}", output_path.display()))?;
Ok(())
}
fn optimize_svg_content(content: &str) -> Result<String> {
let mut optimized = content.to_string();
let comment_regex = Regex::new(r"(?s)<!--.*?-->").context("Failed to compile comment regex")?;
optimized = comment_regex.replace_all(&optimized, "").to_string();
let metadata_regex = Regex::new(r"(?s)<metadata[^>]*>.*?</metadata>")
.context("Failed to compile metadata regex")?;
optimized = metadata_regex.replace_all(&optimized, "").to_string();
let inkscape_regex =
Regex::new(r#"\s*inkscape:[^=]*="[^"]*""#).context("Failed to compile inkscape regex")?;
optimized = inkscape_regex.replace_all(&optimized, "").to_string();
let adobe_regex =
Regex::new(r#"\s*adobe-[^=]*="[^"]*""#).context("Failed to compile adobe regex")?;
optimized = adobe_regex.replace_all(&optimized, "").to_string();
let sodipodi_regex =
Regex::new(r#"\s*sodipodi:[^=]*="[^"]*""#).context("Failed to compile sodipodi regex")?;
optimized = sodipodi_regex.replace_all(&optimized, "").to_string();
let whitespace_regex = Regex::new(r"\s+").context("Failed to compile whitespace regex")?;
optimized = whitespace_regex.replace_all(&optimized, " ").to_string();
optimized = optimized
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<&str>>()
.join("\n");
Ok(optimized)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preserves_essential_svg_elements() {
let input = r#"<?xml version="1.0" encoding="UTF-8"?>
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" fill="blue" />
<rect x="20" y="20" width="60" height="60" fill="red" opacity="0.5" />
<path d="M10 10 L90 90" stroke="black" />
<text x="50" y="50">Hello</text>
<g transform="rotate(45)">
<ellipse cx="30" cy="30" rx="20" ry="15" />
</g>
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("<svg"));
assert!(result.contains("width=\"100\""));
assert!(result.contains("height=\"100\""));
assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
assert!(result.contains("<circle"));
assert!(result.contains("cx=\"50\""));
assert!(result.contains("cy=\"50\""));
assert!(result.contains("r=\"40\""));
assert!(result.contains("fill=\"blue\""));
assert!(result.contains("<rect"));
assert!(result.contains("<path"));
assert!(result.contains("d=\"M10 10 L90 90\""));
assert!(result.contains("<text"));
assert!(result.contains("Hello"));
assert!(result.contains("<g"));
assert!(result.contains("transform=\"rotate(45)\""));
assert!(result.contains("<ellipse"));
assert!(result.contains("</svg>"));
}
#[test]
fn test_removes_comments_and_metadata() {
let input = r#"<?xml version="1.0" encoding="UTF-8"?>
<!-- This is a comment -->
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<rdf:RDF>
<cc:Work>
<dc:title>Test</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<!-- Another comment -->
<circle r="10" />
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(!result.contains("<!-- This is a comment -->"));
assert!(!result.contains("<!-- Another comment -->"));
assert!(!result.contains("<metadata"));
assert!(!result.contains("</metadata>"));
assert!(!result.contains("<rdf:RDF"));
assert!(!result.contains("<cc:Work"));
assert!(!result.contains("<dc:title>"));
assert!(result.contains("<svg"));
assert!(result.contains("<circle"));
assert!(result.contains("r=\"10\""));
assert!(result.contains("</svg>"));
}
#[test]
fn test_removes_editor_specific_attributes() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:version="1.0"
inkscape:current-layer="layer1"
sodipodi:docname="test.svg"
adobe-illustrator-version="25.0">
<circle r="10" inkscape:label="Circle" adobe-blend-mode="normal" />
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(!result.contains("inkscape:version"));
assert!(!result.contains("inkscape:current-layer"));
assert!(!result.contains("sodipodi:docname"));
assert!(!result.contains("adobe-illustrator-version"));
assert!(!result.contains("inkscape:label"));
assert!(!result.contains("adobe-blend-mode"));
assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
assert!(result.contains("xmlns:inkscape"));
assert!(result.contains("<circle"));
assert!(result.contains("r=\"10\""));
}
#[test]
fn test_preserves_style_and_class_attributes() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg">
<style>
.red { fill: red; }
.blue { fill: blue; }
</style>
<circle class="red" style="stroke: black; stroke-width: 2" />
<rect class="blue" style="opacity: 0.8" />
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("<style>"));
assert!(result.contains(".red { fill: red; }"));
assert!(result.contains(".blue { fill: blue; }"));
assert!(result.contains("</style>"));
assert!(result.contains("class=\"red\""));
assert!(result.contains("class=\"blue\""));
assert!(result.contains("style=\"stroke: black; stroke-width: 2\""));
assert!(result.contains("style=\"opacity: 0.8\""));
}
#[test]
fn test_preserves_definitions_and_uses() {
let input = r##"<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1">
<stop offset="0%" stop-color="red" />
<stop offset="100%" stop-color="blue" />
</linearGradient>
<pattern id="pattern1">
<rect width="10" height="10" fill="green" />
</pattern>
</defs>
<rect fill="url(#grad1)" />
<circle fill="url(#pattern1)" />
<use xlink:href="#someElement" />
</svg>"##;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("<defs>"));
assert!(result.contains("</defs>"));
assert!(result.contains("<linearGradient"));
assert!(result.contains("id=\"grad1\""));
assert!(result.contains("<stop"));
assert!(result.contains("stop-color=\"red\""));
assert!(result.contains("<pattern"));
assert!(result.contains("id=\"pattern1\""));
assert!(result.contains("fill=\"url("));
assert!(result.contains("grad1)\""));
assert!(result.contains("pattern1)\""));
assert!(result.contains("<use"));
assert!(result.contains("xlink:href=\""));
assert!(result.contains("someElement\""));
}
#[test]
fn test_preserves_animations() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg">
<circle r="10">
<animate attributeName="r" values="10;20;10" dur="2s" repeatCount="indefinite" />
<animateTransform attributeName="transform" type="rotate"
values="0;360" dur="1s" repeatCount="indefinite" />
</circle>
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("<animate"));
assert!(result.contains("attributeName=\"r\""));
assert!(result.contains("values=\"10;20;10\""));
assert!(result.contains("dur=\"2s\""));
assert!(result.contains("repeatCount=\"indefinite\""));
assert!(result.contains("<animateTransform"));
assert!(result.contains("type=\"rotate\""));
assert!(result.contains("values=\"0;360\""));
}
#[test]
fn test_normalizes_whitespace_but_preserves_structure() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg" >
<circle cx="50" cy="50" r="40" />
<rect x="10" y="10" />
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("<svg"));
assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
assert!(result.contains("<circle"));
assert!(result.contains("cx=\"50\""));
assert!(result.contains("cy=\"50\""));
assert!(result.contains("r=\"40\""));
assert!(result.contains("<rect"));
assert!(result.contains("x=\"10\""));
assert!(result.contains("y=\"10\""));
assert!(result.contains("</svg>"));
assert!(!result.contains(" xmlns"));
assert!(!result.contains(" cx"));
assert!(!result.contains("\n\n\n"));
}
#[test]
fn test_handles_multiline_comments() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg">
<!--
This is a multiline comment
that spans multiple lines
and should be removed
-->
<circle r="10" />
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(!result.contains("This is a multiline comment"));
assert!(!result.contains("that spans multiple lines"));
assert!(!result.contains("and should be removed"));
assert!(result.contains("<svg"));
assert!(result.contains("<circle"));
assert!(result.contains("r=\"10\""));
}
#[test]
fn test_preserves_viewbox_and_coordinate_systems() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 200"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(50, 50) scale(2)">
<circle r="10" />
</g>
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("viewBox=\"0 0 200 200\""));
assert!(result.contains("preserveAspectRatio=\"xMidYMid meet\""));
assert!(result.contains("transform=\"translate(50, 50) scale(2)\""));
}
#[test]
fn test_empty_svg_handled_gracefully() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg"></svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("<svg"));
assert!(result.contains("xmlns=\"http://www.w3.org/2000/svg\""));
assert!(result.contains("</svg>"));
}
#[test]
fn test_preserves_data_attributes_and_ids() {
let input = r#"<svg xmlns="http://www.w3.org/2000/svg" data-name="icon">
<circle id="main-circle" data-value="42" class="important" />
<rect id="background" data-layer="base" />
</svg>"#;
let result = optimize_svg_content(input).unwrap();
assert!(result.contains("data-name=\"icon\""));
assert!(result.contains("id=\"main-circle\""));
assert!(result.contains("data-value=\"42\""));
assert!(result.contains("class=\"important\""));
assert!(result.contains("id=\"background\""));
assert!(result.contains("data-layer=\"base\""));
}
}