use crate::numeric::{affine_length, fmt_num, parse_num_list, scale_len_list, scale_length};
pub fn attr(key: &str, val: &str, s: f64, tx: f64, ty: f64) -> String {
match key {
"transform" | "gradientTransform" | "patternTransform" => transform(val, s, tx, ty),
"d" => path_data(val, s, tx, ty),
"points" => points(val, s, tx, ty),
"stroke-dasharray" | "dx" | "dy" => scale_len_list(val, s),
"x" | "cx" | "x1" | "x2" | "fx" => affine_length(val, s, tx),
"y" | "cy" | "y1" | "y2" | "fy" => affine_length(val, s, ty),
"width" | "height" | "r" | "rx" | "ry" | "stroke-width" | "stroke-dashoffset"
| "font-size" => scale_length(val, s),
_ => val.to_string(),
}
}
pub fn implicit_origins(local: &str) -> &'static [&'static str] {
match local {
"rect" | "image" | "use" | "svg" | "foreignObject" => &["x", "y"],
"circle" | "ellipse" => &["cx", "cy"],
"line" => &["x1", "y1", "x2", "y2"],
_ => &[],
}
}
pub fn origin_value(key: &str, tx: f64, ty: f64) -> String {
match key {
"y" | "cy" | "y1" | "y2" => fmt_num(ty),
_ => fmt_num(tx),
}
}
fn points(v: &str, s: f64, tx: f64, ty: f64) -> String {
parse_num_list(v)
.chunks(2)
.map(|c| {
if c.len() == 2 {
format!("{} {}", fmt_num(s * c[0] + tx), fmt_num(s * c[1] + ty))
} else {
fmt_num(s * c[0] + tx)
}
})
.collect::<Vec<_>>()
.join(" ")
}
fn transform(v: &str, s: f64, tx: f64, ty: f64) -> String {
let mut out = String::new();
let mut rest = v;
loop {
let trimmed = rest.trim_start_matches([' ', ',', '\t', '\n', '\r']);
if trimmed.is_empty() {
break;
}
let Some(open) = trimmed.find('(') else {
out.push_str(trimmed);
break;
};
let Some(close) = trimmed[open + 1..].find(')') else {
out.push_str(trimmed);
break;
};
let name = trimmed[..open].trim();
let args = parse_num_list(&trimmed[open + 1..open + 1 + close]);
if !out.is_empty() {
out.push(' ');
}
out.push_str(&conjugate(name, &args, s, tx, ty));
rest = &trimmed[open + 1 + close + 1..];
}
out
}
fn conjugate(name: &str, args: &[f64], s: f64, tx: f64, ty: f64) -> String {
let g = |i: usize, d: f64| args.get(i).copied().unwrap_or(d);
match name {
"translate" => {
let (dx, dy) = (g(0, 0.0), g(1, 0.0));
format!("translate({} {})", fmt_num(s * dx), fmt_num(s * dy))
}
"rotate" => {
let (a, cx, cy) = (g(0, 0.0), g(1, 0.0), g(2, 0.0));
format!(
"rotate({} {} {})",
fmt_num(a),
fmt_num(s * cx + tx),
fmt_num(s * cy + ty)
)
}
"scale" => {
let a = g(0, 1.0);
let b = g(1, a);
matrix_str(a, 0.0, 0.0, b, (1.0 - a) * tx, (1.0 - b) * ty)
}
"skewX" => {
let c = (g(0, 0.0).to_radians()).tan();
matrix_str(1.0, 0.0, c, 1.0, -c * ty, 0.0)
}
"skewY" => {
let b = (g(0, 0.0).to_radians()).tan();
matrix_str(1.0, b, 0.0, 1.0, 0.0, -b * tx)
}
"matrix" => {
let (a, b, c, d, e, f) = (
g(0, 1.0),
g(1, 0.0),
g(2, 0.0),
g(3, 1.0),
g(4, 0.0),
g(5, 0.0),
);
let e2 = s * e + (1.0 - a) * tx - c * ty;
let f2 = s * f - b * tx + (1.0 - d) * ty;
matrix_str(a, b, c, d, e2, f2)
}
_ => {
let joined = args
.iter()
.map(|a| fmt_num(*a))
.collect::<Vec<_>>()
.join(" ");
format!("{name}({joined})")
}
}
}
fn matrix_str(a: f64, b: f64, c: f64, d: f64, e: f64, f: f64) -> String {
format!(
"matrix({} {} {} {} {} {})",
fmt_num(a),
fmt_num(b),
fmt_num(c),
fmt_num(d),
fmt_num(e),
fmt_num(f)
)
}
fn path_data(d: &str, s: f64, tx: f64, ty: f64) -> String {
use svgtypes::{PathParser, PathSegment};
let pt = |x: f64, y: f64, abs: bool| {
if abs {
(s * x + tx, s * y + ty)
} else {
(s * x, s * y)
}
};
let mut out = String::new();
let mut first = true;
for seg in PathParser::from(d) {
let Ok(seg) = seg else {
return d.to_string();
};
match seg {
PathSegment::MoveTo { abs, x, y } => {
let absolute = abs || first;
let (x, y) = pt(x, y, absolute);
cmd(&mut out, 'M', absolute);
out.push_str(&two(x, y));
}
PathSegment::LineTo { abs, x, y } => {
let (x, y) = pt(x, y, abs);
cmd(&mut out, 'L', abs);
out.push_str(&two(x, y));
}
PathSegment::HorizontalLineTo { abs, x } => {
let x = if abs { s * x + tx } else { s * x };
cmd(&mut out, 'H', abs);
out.push_str(&fmt_num(x));
}
PathSegment::VerticalLineTo { abs, y } => {
let y = if abs { s * y + ty } else { s * y };
cmd(&mut out, 'V', abs);
out.push_str(&fmt_num(y));
}
PathSegment::CurveTo {
abs,
x1,
y1,
x2,
y2,
x,
y,
} => {
let (x1, y1) = pt(x1, y1, abs);
let (x2, y2) = pt(x2, y2, abs);
let (x, y) = pt(x, y, abs);
cmd(&mut out, 'C', abs);
out.push_str(&nums(&[x1, y1, x2, y2, x, y]));
}
PathSegment::SmoothCurveTo { abs, x2, y2, x, y } => {
let (x2, y2) = pt(x2, y2, abs);
let (x, y) = pt(x, y, abs);
cmd(&mut out, 'S', abs);
out.push_str(&nums(&[x2, y2, x, y]));
}
PathSegment::Quadratic { abs, x1, y1, x, y } => {
let (x1, y1) = pt(x1, y1, abs);
let (x, y) = pt(x, y, abs);
cmd(&mut out, 'Q', abs);
out.push_str(&nums(&[x1, y1, x, y]));
}
PathSegment::SmoothQuadratic { abs, x, y } => {
let (x, y) = pt(x, y, abs);
cmd(&mut out, 'T', abs);
out.push_str(&two(x, y));
}
PathSegment::EllipticalArc {
abs,
rx,
ry,
x_axis_rotation,
large_arc,
sweep,
x,
y,
} => {
let (x, y) = pt(x, y, abs);
cmd(&mut out, 'A', abs);
out.push_str(&nums(&[rx * s, ry * s, x_axis_rotation]));
out.push(' ');
out.push(if large_arc { '1' } else { '0' });
out.push(' ');
out.push(if sweep { '1' } else { '0' });
out.push(' ');
out.push_str(&two(x, y));
}
PathSegment::ClosePath { abs } => cmd(&mut out, 'Z', abs),
}
first = false;
out.push(' ');
}
out.trim_end().to_string()
}
fn cmd(out: &mut String, upper: char, abs: bool) {
out.push(if abs {
upper
} else {
upper.to_ascii_lowercase()
});
out.push(' ');
}
fn two(a: f64, b: f64) -> String {
nums(&[a, b])
}
fn nums(vs: &[f64]) -> String {
vs.iter().map(|v| fmt_num(*v)).collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_absolute_and_relative() {
let out = path_data("M10 10 l5 0", 2.0, 3.0, 4.0);
assert_eq!(out, "M 23 24 l 10 0");
}
#[test]
fn path_leading_relative_moveto_is_absolute() {
let out = path_data("m10 10 l5 0", 2.0, 3.0, 4.0);
assert_eq!(out, "M 23 24 l 10 0");
let out = path_data("M0 0 m10 10", 2.0, 3.0, 4.0);
assert_eq!(out, "M 3 4 m 20 20");
}
#[test]
fn transform_translate_and_rotate() {
assert_eq!(
transform("translate(5 5)", 2.0, 3.0, 4.0),
"translate(10 10)"
);
assert_eq!(transform("rotate(90)", 2.0, 3.0, 4.0), "rotate(90 3 4)");
}
#[test]
fn scale_only_keeps_translate_and_rotate() {
assert_eq!(
transform("translate(10 20)", 2.0, 0.0, 0.0),
"translate(20 40)"
);
assert_eq!(transform("rotate(45 4 4)", 2.0, 0.0, 0.0), "rotate(45 8 8)");
}
#[test]
fn arc_keeps_its_flags() {
let out = path_data("M0 0 A5 5 0 0 1 10 10", 2.0, 0.0, 0.0);
assert_eq!(out, "M 0 0 A 10 10 0 0 1 20 20");
}
}