use anyhow::{Context as _, Result, anyhow};
use quick_xml::events::{BytesStart, Event};
use quick_xml::reader::Reader;
use std::collections::HashSet;
use crate::numeric::{affine_length, fmt_num, parse_num_list, scale_len_list, scale_length};
pub fn use_targets(src: &str) -> Result<HashSet<String>> {
let mut reader = Reader::from_str(src);
let mut ids = HashSet::new();
loop {
let element = match reader.read_event().context("parsing SVG")? {
Event::Eof => break,
Event::Start(element) | Event::Empty(element) => element,
_ => continue,
};
if element.local_name().into_inner() != "use" {
continue;
}
for key in ["href", "xlink:href"] {
if let Some(href) = attr_value(&element, key)?
&& let Some(id) = href.trim().strip_prefix('#')
{
ids.insert(id.to_owned());
}
}
}
return Ok(ids);
}
pub fn is_template(element: &BytesStart, templates: &HashSet<String>) -> Result<bool> {
if element.local_name().into_inner() == "symbol" {
return Ok(true);
}
return Ok(attr_value(element, "id")?.is_some_and(|id| return templates.contains(&id)));
}
pub fn attr_value(element: &BytesStart, key: &str) -> Result<Option<String>> {
for attr in element.attributes() {
let attr = attr.map_err(|err| anyhow!("attribute: {err}"))?;
if attr.key.as_ref() == key {
return Ok(Some(
attr.normalized_value(quick_xml::XmlVersion::Implicit1_0)?
.into_owned(),
));
}
}
return Ok(None);
}
pub fn attr(key: &str, val: &str, scale: f64, tx: f64, ty: f64) -> String {
match key {
"transform" | "gradientTransform" | "patternTransform" => {
return transform(val, scale, tx, ty);
}
"d" => return path_data(val, scale, tx, ty),
"points" => return points(val, scale, tx, ty),
"stroke-dasharray" | "dx" | "dy" => return scale_len_list(val, scale),
"x" | "cx" | "x1" | "x2" | "fx" => return affine_length(val, scale, tx),
"y" | "cy" | "y1" | "y2" | "fy" => return affine_length(val, scale, ty),
"width" | "height" | "r" | "rx" | "ry" | "stroke-width" | "stroke-dashoffset"
| "font-size" => return scale_length(val, scale),
_ => return val.to_owned(),
}
}
pub fn implicit_origins(local: &str) -> &'static [&'static str] {
match local {
"rect" | "image" | "use" | "svg" | "foreignObject" => return &["x", "y"],
"circle" | "ellipse" => return &["cx", "cy"],
"line" => return &["x1", "y1", "x2", "y2"],
_ => return &[],
}
}
pub fn origin_value(key: &str, tx: f64, ty: f64) -> String {
match key {
"y" | "cy" | "y1" | "y2" => return fmt_num(ty),
_ => return fmt_num(tx),
}
}
fn points(val: &str, scale: f64, tx: f64, ty: f64) -> String {
return parse_num_list(val)
.chunks(2)
.map(|pair| {
if pair.len() == 2 {
return format!(
"{} {}",
fmt_num(scale * pair[0] + tx),
fmt_num(scale * pair[1] + ty)
);
}
return fmt_num(scale * pair[0] + tx);
})
.collect::<Vec<_>>()
.join(" ");
}
fn transform(val: &str, scale: f64, tx: f64, ty: f64) -> String {
let mut out = String::new();
let mut rest = val;
loop {
let trimmed = rest.trim_start_matches([' ', ',', '\t', '\n', '\r']);
if trimmed.is_empty() {
break;
}
let Some((name, after)) = trimmed.split_once('(') else {
out.push_str(trimmed);
break;
};
let Some((inner, tail)) = after.split_once(')') else {
out.push_str(trimmed);
break;
};
let name = name.trim();
let args = parse_num_list(inner);
if !out.is_empty() {
out.push(' ');
}
out.push_str(&conjugate(name, &args, scale, tx, ty));
rest = tail;
}
return out;
}
fn conjugate(name: &str, args: &[f64], scale: f64, tx: f64, ty: f64) -> String {
let arg = |i: usize, default: f64| return args.get(i).copied().unwrap_or(default);
match name {
"translate" => {
let (dx, dy) = (arg(0, 0.0), arg(1, 0.0));
return format!("translate({} {})", fmt_num(scale * dx), fmt_num(scale * dy));
}
"rotate" => {
let (angle, cx, cy) = (arg(0, 0.0), arg(1, 0.0), arg(2, 0.0));
return format!(
"rotate({} {} {})",
fmt_num(angle),
fmt_num(scale * cx + tx),
fmt_num(scale * cy + ty)
);
}
"scale" => {
let sx = arg(0, 1.0);
let sy = arg(1, sx);
return matrix_str([sx, 0.0, 0.0, sy, (1.0 - sx) * tx, (1.0 - sy) * ty]);
}
"skewX" => {
let tan = (arg(0, 0.0).to_radians()).tan();
return matrix_str([1.0, 0.0, tan, 1.0, -tan * ty, 0.0]);
}
"skewY" => {
let tan = (arg(0, 0.0).to_radians()).tan();
return matrix_str([1.0, tan, 0.0, 1.0, 0.0, -tan * tx]);
}
"matrix" => {
let (ma, mb, mc, md, me, mf) = (
arg(0, 1.0),
arg(1, 0.0),
arg(2, 0.0),
arg(3, 1.0),
arg(4, 0.0),
arg(5, 0.0),
);
let me2 = scale * me + (1.0 - ma) * tx - mc * ty;
let mf2 = scale * mf - mb * tx + (1.0 - md) * ty;
return matrix_str([ma, mb, mc, md, me2, mf2]);
}
_ => {
let joined = args
.iter()
.map(|val| return fmt_num(*val))
.collect::<Vec<_>>()
.join(" ");
return format!("{name}({joined})");
}
}
}
fn matrix_str(values: [f64; 6]) -> String {
return format!("matrix({})", nums(&values));
}
fn path_data(data: &str, scale: f64, tx: f64, ty: f64) -> String {
use svgtypes::{PathParser, PathSegment};
let pt = |x: f64, y: f64, abs: bool| {
if abs {
return (scale * x + tx, scale * y + ty);
}
return (scale * x, scale * y);
};
let mut out = String::new();
let mut first = true;
for seg in PathParser::from(data) {
let Ok(seg) = seg else {
return data.to_owned();
};
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 { scale * x + tx } else { scale * x };
cmd(&mut out, 'H', abs);
out.push_str(&fmt_num(x));
}
PathSegment::VerticalLineTo { abs, y } => {
let y = if abs { scale * y + ty } else { scale * 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 * scale, ry * scale, 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(' ');
}
return out.trim_end().to_owned();
}
fn cmd(out: &mut String, upper: char, abs: bool) {
out.push(if abs {
upper
} else {
upper.to_ascii_lowercase()
});
out.push(' ');
}
fn two(first: f64, second: f64) -> String {
return nums(&[first, second]);
}
fn nums(values: &[f64]) -> String {
return values
.iter()
.map(|val| return fmt_num(*val))
.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");
}
}