use regex::Regex;
const SHAPE_TAG: &str = r#"((?:path|polygon|rect|line)[^>]*?)\bd="([^"]*)"([^>]*?)\bstyle="([^"]*)""#;
const TRANSFORM_VALUE: &str = r#"\btransform="([^"]*)""#;
const POINTS_VALUE: &str = r#"\bpoints="([^"]*)""#;
const NUMBER: &str = r"-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?";
pub fn optimize(svg: &str) -> String {
if svg.is_empty() {
return svg.to_string();
}
let re_shape = Regex::new(SHAPE_TAG).expect("shape 正则编译失败");
let out1 = re_shape
.replace_all(svg, |caps: ®ex::Captures| {
let d = round_attribute_value(&caps[2], if caps[4].contains("stroke") { 2 } else { 1 });
format!("{} d=\"{}\"{} style=\"{}\"", &caps[1], d, &caps[3], &caps[4])
})
.into_owned();
let re_transform = Regex::new(TRANSFORM_VALUE).expect("transform 正则编译失败");
let out2 = re_transform
.replace_all(&out1, |caps: ®ex::Captures| {
format!("transform=\"{}\"", round_attribute_value(&caps[1], 1))
})
.into_owned();
let re_points = Regex::new(POINTS_VALUE).expect("points 正则编译失败");
let out3 = re_points
.replace_all(&out2, |caps: ®ex::Captures| {
format!("points=\"{}\"", round_attribute_value(&caps[1], 1))
})
.into_owned();
out3.replace(";\"", "\"")
}
fn round_attribute_value(value: &str, decimals: usize) -> String {
let re = Regex::new(NUMBER).expect("数字正则编译失败");
re.replace_all(value, |caps: ®ex::Captures| round_token(&caps[0], decimals))
.into_owned()
}
pub fn round_token(token: &str, decimals: usize) -> String {
let v: f64 = match token.parse() {
Ok(v) => v,
Err(_) => return token.to_string(),
};
let mul = 10f64.powi(decimals as i32);
let rounded = (v * mul + 0.5).floor();
if rounded == 0.0 {
return if token.starts_with('-') { "-0".to_string() } else { "0".to_string() };
}
let neg = rounded < 0.0;
let scaled_abs = rounded.abs();
let pow = 10u64.pow(decimals as u32);
let int_part = (scaled_abs as u64) / pow;
let frac = (scaled_abs as u64) % pow;
let mut s = if neg {
format!("-{}", int_part)
} else {
format!("{}", int_part)
};
if frac > 0 {
s.push('.');
let mut frac_s = format!("{:0width$}", frac, width = decimals);
while frac_s.ends_with('0') {
frac_s.pop();
}
s.push_str(&frac_s);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_one_decimal() {
assert_eq!(round_token("12.34", 1), "12.3");
assert_eq!(round_token("12.35", 1), "12.4"); assert_eq!(round_token("2.0", 1), "2");
assert_eq!(round_token("0.05", 1), "0.1");
}
#[test]
fn round_two_decimal() {
assert_eq!(round_token("1.234", 2), "1.23");
assert_eq!(round_token("1.235", 2), "1.24");
assert_eq!(round_token("1.50", 2), "1.5");
}
#[test]
fn negative_zero_keeps_sign() {
assert_eq!(round_token("-0.0428", 1), "-0");
assert_eq!(round_token("0.0428", 1), "0");
assert_eq!(round_token("-0.0428", 2), "-0.04");
}
#[test]
fn negative_values() {
assert_eq!(round_token("-2.35", 1), "-2.3");
assert_eq!(round_token("-2.34", 1), "-2.3");
assert_eq!(round_token("-63.65", 1), "-63.6"); }
#[test]
fn optimize_strips_trailing_semicolon() {
let svg = "<path d=\"1.234 2.345\" style=\"fill:#333;\"/>";
let out = optimize(svg);
assert_eq!(out, "<path d=\"1.2 2.3\" style=\"fill:#333\"/>");
}
#[test]
fn optimize_stroke_keeps_two_decimals() {
let svg = "<path d=\"1.234 2.345\" style=\"fill:none;stroke:#000;stroke-width:2\"/>";
let out = optimize(svg);
assert_eq!(
out,
"<path d=\"1.23 2.35\" style=\"fill:none;stroke:#000;stroke-width:2\"/>"
);
}
#[test]
fn optimize_transform_and_points() {
let svg = "<polygon points=\"0.05 1.05, 2.25 3.35\" transform=\"matrix(1 0 0 .99987 4e-5 -3e-5)\" style=\"fill:#fff;\"/>";
let out = optimize(svg);
assert_eq!(
out,
"<polygon points=\"0.1 1.1, 2.3 3.4\" transform=\"matrix(1 0 0 .99987 0 -0)\" style=\"fill:#fff\"/>"
);
}
}