use std::collections::HashMap;
use teksilo_tokens::Color;
use crate::geometry::{Point, Rect, Transform2D};
use crate::xml::XmlElement;
use super::color::{parse_alpha, parse_color};
use super::{parse_inline_style, parse_transform};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SvgStop {
pub offset: f32,
pub color: Option<Color>,
pub opacity: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GradientUnits {
ObjectBoundingBox,
UserSpaceOnUse,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum GradientGeometry {
Linear { x1: f32, y1: f32, x2: f32, y2: f32 },
Radial { cx: f32, cy: f32, r: f32 },
}
#[derive(Debug, Clone)]
pub(crate) struct GradientDef {
geometry: GradientGeometry,
stops: Vec<SvgStop>,
units: GradientUnits,
transform: Transform2D,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ResolvedGradient {
Linear {
start: Point,
end: Point,
stops: Vec<SvgStop>,
},
Radial {
center: Point,
radius: f32,
stops: Vec<SvgStop>,
},
}
const MAX_HREF_DEPTH: usize = 16;
impl GradientDef {
pub(crate) fn resolve(
&self,
bbox: Rect,
to_view_box: &Transform2D,
) -> Option<ResolvedGradient> {
if self.stops.is_empty() {
return None;
}
let to_local = match self.units {
GradientUnits::UserSpaceOnUse => self.transform,
GradientUnits::ObjectBoundingBox => {
if bbox.width <= 0.0 || bbox.height <= 0.0 {
return None;
}
let unit_to_bbox = Transform2D::scale(bbox.width, bbox.height)
.then(&Transform2D::translate(bbox.x, bbox.y));
self.transform.then(&unit_to_bbox)
}
};
let m = to_local.then(to_view_box);
let stops = if self.stops.len() == 1 {
let only = self.stops[0];
vec![
SvgStop {
offset: 0.0,
..only
},
SvgStop {
offset: 1.0,
..only
},
]
} else {
self.stops.clone()
};
Some(match self.geometry {
GradientGeometry::Linear { x1, y1, x2, y2 } => ResolvedGradient::Linear {
start: m.apply_point(Point::new(x1, y1)),
end: m.apply_point(Point::new(x2, y2)),
stops,
},
GradientGeometry::Radial { cx, cy, r } => ResolvedGradient::Radial {
center: m.apply_point(Point::new(cx, cy)),
radius: r * m.geometric_scale(),
stops,
},
})
}
}
pub(crate) fn collect_gradients<'a>(
id_map: &HashMap<&'a str, &'a XmlElement>,
view_box: Rect,
) -> HashMap<String, GradientDef> {
let mut out = HashMap::new();
for (id, el) in id_map {
let tag = el.tag_name();
if tag != "linearGradient" && tag != "radialGradient" {
continue;
}
if let Some(def) = parse_gradient(el, id_map, view_box, 0) {
out.insert((*id).to_string(), def);
}
}
out
}
fn parse_gradient<'a>(
el: &'a XmlElement,
id_map: &HashMap<&'a str, &'a XmlElement>,
view_box: Rect,
depth: usize,
) -> Option<GradientDef> {
let inherited = if depth < MAX_HREF_DEPTH {
el.attribute("href")
.or_else(|| el.attribute("xlink:href"))
.and_then(|h| h.strip_prefix('#'))
.and_then(|id| id_map.get(id))
.and_then(|target| parse_gradient(target, id_map, view_box, depth + 1))
} else {
None
};
let is_radial = el.tag_name() == "radialGradient";
let units = match el.attribute("gradientUnits") {
Some(u) if u.trim() == "userSpaceOnUse" => GradientUnits::UserSpaceOnUse,
Some(_) => GradientUnits::ObjectBoundingBox,
None => inherited
.as_ref()
.map(|g| g.units)
.unwrap_or(GradientUnits::ObjectBoundingBox),
};
let axis = |v: &str, span: f32| -> Option<f32> {
let t = v.trim();
match t.strip_suffix('%') {
Some(p) => {
let frac = p.trim().parse::<f32>().ok()? / 100.0;
Some(match units {
GradientUnits::ObjectBoundingBox => frac,
GradientUnits::UserSpaceOnUse => frac * span,
})
}
None => t.parse::<f32>().ok(),
}
};
let get =
|name: &str, span: f32| -> Option<f32> { el.attribute(name).and_then(|v| axis(v, span)) };
let diag = ((view_box.width.powi(2) + view_box.height.powi(2)) / 2.0).sqrt();
let inherited_geo = inherited.as_ref().map(|g| g.geometry);
let geometry = if is_radial {
let (icx, icy, ir) = match inherited_geo {
Some(GradientGeometry::Radial { cx, cy, r }) => (Some(cx), Some(cy), Some(r)),
_ => (None, None, None),
};
let default = |frac: f32, span: f32| match units {
GradientUnits::ObjectBoundingBox => frac,
GradientUnits::UserSpaceOnUse => frac * span,
};
GradientGeometry::Radial {
cx: get("cx", view_box.width)
.or(icx)
.unwrap_or_else(|| default(0.5, view_box.width)),
cy: get("cy", view_box.height)
.or(icy)
.unwrap_or_else(|| default(0.5, view_box.height)),
r: get("r", diag).or(ir).unwrap_or_else(|| default(0.5, diag)),
}
} else {
let (ix1, iy1, ix2, iy2) = match inherited_geo {
Some(GradientGeometry::Linear { x1, y1, x2, y2 }) => {
(Some(x1), Some(y1), Some(x2), Some(y2))
}
_ => (None, None, None, None),
};
let default = |frac: f32, span: f32| match units {
GradientUnits::ObjectBoundingBox => frac,
GradientUnits::UserSpaceOnUse => frac * span,
};
GradientGeometry::Linear {
x1: get("x1", view_box.width)
.or(ix1)
.unwrap_or_else(|| default(0.0, view_box.width)),
y1: get("y1", view_box.height)
.or(iy1)
.unwrap_or_else(|| default(0.0, view_box.height)),
x2: get("x2", view_box.width)
.or(ix2)
.unwrap_or_else(|| default(1.0, view_box.width)),
y2: get("y2", view_box.height)
.or(iy2)
.unwrap_or_else(|| default(0.0, view_box.height)),
}
};
let transform = el
.attribute("gradientTransform")
.and_then(|t| parse_transform(t).ok())
.or_else(|| inherited.as_ref().map(|g| g.transform))
.unwrap_or(Transform2D::IDENTITY);
let mut stops = parse_stops(el);
if stops.is_empty() {
stops = inherited.map(|g| g.stops).unwrap_or_default();
}
Some(GradientDef {
geometry,
stops,
units,
transform,
})
}
fn parse_stops(gradient: &XmlElement) -> Vec<SvgStop> {
let mut out: Vec<SvgStop> = Vec::new();
for stop in gradient.children().filter(|c| c.tag_name() == "stop") {
let read_color = |v: &str| -> Option<Option<Color>> {
if v.trim().eq_ignore_ascii_case("currentcolor") {
Some(None)
} else {
parse_color(v).map(Some)
}
};
let mut color = stop
.attribute("stop-color")
.and_then(read_color)
.unwrap_or(Some(Color::BLACK));
let mut opacity = stop
.attribute("stop-opacity")
.and_then(parse_alpha)
.unwrap_or(1.0);
if let Some(style) = stop.attribute("style") {
for (key, value) in parse_inline_style(style) {
match key {
"stop-color" => {
if let Some(c) = read_color(value) {
color = c;
}
}
"stop-opacity" => {
if let Some(o) = parse_alpha(value) {
opacity = o;
}
}
_ => {}
}
}
}
let offset = stop
.attribute("offset")
.and_then(|o| {
let t = o.trim();
match t.strip_suffix('%') {
Some(p) => p.trim().parse::<f32>().ok().map(|v| v / 100.0),
None => t.parse::<f32>().ok(),
}
})
.unwrap_or(0.0)
.clamp(0.0, 1.0);
let offset = match out.last() {
Some(prev) => offset.max(prev.offset),
None => offset,
};
out.push(SvgStop {
offset,
color,
opacity,
});
}
out
}