pub fn optimize(svg: &str) -> String {
if svg.is_empty() {
return svg.to_string();
}
let out1 = replace_shapes(svg);
let out2 = replace_attribute(&out1, "transform", 1);
let out3 = replace_attribute(&out2, "points", 1);
out3.replace(";\"", "\"")
}
fn replace_shapes(svg: &str) -> String {
let mut out = String::with_capacity(svg.len());
let mut rest = svg;
while let Some(pos) = rest.find('<') {
out.push_str(&rest[..pos]);
let after_lt = &rest[pos + 1..];
if let Some((tag, after_tag)) = shape_tag(after_lt) {
if let Some((pre1, d_val, tail1)) = find_attr(after_tag, "d", true) {
if let Some((pre2, style_val, tail2)) = find_attr(tail1, "style", true) {
let d_rounded =
round_attribute_value(d_val, if style_val.contains("stroke") { 2 } else { 1 });
out.push('<');
out.push_str(tag);
out.push_str(pre1);
out.push_str(" d=\"");
out.push_str(&d_rounded);
out.push('"');
out.push_str(pre2);
out.push_str(" style=\"");
out.push_str(style_val);
out.push('"');
rest = tail2;
continue;
}
}
}
out.push('<');
rest = after_lt;
}
out.push_str(rest);
out
}
fn shape_tag(s: &str) -> Option<(&str, &str)> {
for tag in ["polygon", "path", "rect", "line"] {
if s.starts_with(tag) {
return Some((tag, &s[tag.len()..]));
}
}
None
}
fn replace_attribute(svg: &str, attr: &str, decimals: usize) -> String {
let mut out = String::with_capacity(svg.len());
let mut rest = svg;
loop {
match find_attr(rest, attr, false) {
Some((pre, val, tail)) => {
out.push_str(pre);
out.push_str(attr);
out.push_str("=\"");
out.push_str(&round_attribute_value(val, decimals));
out.push('"');
rest = tail;
}
None => {
out.push_str(rest);
break;
}
}
}
out
}
fn find_attr<'a>(s: &'a str, attr: &str, stop_at_gt: bool) -> Option<(&'a str, &'a str, &'a str)> {
let b = s.as_bytes();
let mut i = 0;
while i < b.len() {
if stop_at_gt && b[i] == b'>' {
return None;
}
if s[i..].starts_with(attr) && word_boundary_before(s, i) {
let after = i + attr.len();
if s[after..].starts_with("=\"") {
let vstart = after + 2;
if let Some(vend_rel) = s[vstart..].find('"') {
let vend = vstart + vend_rel;
return Some((&s[..i], &s[vstart..vend], &s[vend + 1..]));
}
return None;
}
}
i += 1;
}
None
}
fn word_boundary_before(s: &str, i: usize) -> bool {
if i == 0 {
return true;
}
let c = s.as_bytes()[i - 1];
!(c.is_ascii_alphanumeric() || c == b'_')
}
fn round_attribute_value(value: &str, decimals: usize) -> String {
let b = value.as_bytes();
let mut out = String::with_capacity(value.len());
let mut i = 0;
while i < b.len() {
let neg = b[i] == b'-' && i + 1 < b.len() && b[i + 1].is_ascii_digit();
if b[i].is_ascii_digit() || neg {
let start = i;
let mut j = i;
if neg {
j += 1;
}
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
if j + 1 < b.len() && b[j] == b'.' && b[j + 1].is_ascii_digit() {
j += 1;
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
}
if j < b.len() && (b[j] == b'e' || b[j] == b'E') {
let mut k = j + 1;
if k < b.len() && (b[k] == b'+' || b[k] == b'-') {
k += 1;
}
if k < b.len() && b[k].is_ascii_digit() {
j = k;
while j < b.len() && b[j].is_ascii_digit() {
j += 1;
}
}
}
out.push_str(&round_token(&value[start..j], decimals));
i = j;
} else {
let ch = value[i..].chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
}
out
}
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\"/>"
);
}
}